@leo000001/opencode-quota-sidebar 4.0.11 → 4.0.13

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/dist/cli.js CHANGED
@@ -13,6 +13,30 @@ import { authFilePath, loadConfig, loadState,
13
13
  quotaConfigPaths, resolveOpencodeDataDir, stateFilePath, } from './storage.js';
14
14
  import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, listCurrentProviderIDs, } from './provider_catalog.js';
15
15
  import { createUsageService } from './usage_service.js';
16
+ function emptyProviderUsage(usage) {
17
+ return {
18
+ ...usage,
19
+ providers: {},
20
+ };
21
+ }
22
+ function strictFilterUsageProviders(usage, allowedProviderIDs) {
23
+ if (allowedProviderIDs.size === 0)
24
+ return emptyProviderUsage(usage);
25
+ return filterUsageProvidersForDisplay(usage, allowedProviderIDs);
26
+ }
27
+ function strictFilterHistoryProviders(history, allowedProviderIDs) {
28
+ if (allowedProviderIDs.size === 0) {
29
+ return {
30
+ ...history,
31
+ rows: history.rows.map((row) => ({
32
+ ...row,
33
+ usage: emptyProviderUsage(row.usage),
34
+ })),
35
+ total: emptyProviderUsage(history.total),
36
+ };
37
+ }
38
+ return filterHistoryProvidersForDisplay(history, allowedProviderIDs);
39
+ }
16
40
  const DEFAULT_OPENCODE_BASE_URL = 'http://localhost:4096';
17
41
  const CLI_SERVER_TIMEOUT_MS = 10_000;
18
42
  const CLI_FORCE_EXIT_DELAY_MS = 100;
@@ -407,17 +431,15 @@ export async function runCli(argv) {
407
431
  listDescendantSessionIDs: async () => [],
408
432
  },
409
433
  });
410
- const quotas = await quotaService.getQuotaSnapshots([], {
411
- allowDefault: true,
412
- });
413
434
  const allowedProviderIDs = await listCurrentProviderIDs({
414
435
  client,
415
436
  directory,
416
- }).catch(() => new Set());
437
+ });
417
438
  if (command.since || command.last !== undefined) {
418
439
  const resolvedSince = command.since || sinceFromLast(command.period, command.last);
419
440
  const historyRaw = await usageService.summarizeHistoryUsage(command.period, resolvedSince);
420
- const history = filterHistoryProvidersForDisplay(historyRaw, allowedProviderIDs);
441
+ const history = strictFilterHistoryProviders(historyRaw, allowedProviderIDs);
442
+ const quotas = await quotaService.getQuotaSnapshots(Object.keys(history.total.providers));
421
443
  return renderCliHistoryDashboard({
422
444
  result: history,
423
445
  quotas,
@@ -426,7 +448,8 @@ export async function runCli(argv) {
426
448
  });
427
449
  }
428
450
  const usageRaw = await usageService.summarizeForTool(command.period, '', false);
429
- const usage = filterUsageProvidersForDisplay(usageRaw, allowedProviderIDs);
451
+ const usage = strictFilterUsageProviders(usageRaw, allowedProviderIDs);
452
+ const quotas = await quotaService.getQuotaSnapshots(Object.keys(usage.providers));
430
453
  return renderCliDashboard({
431
454
  label: cliCurrentLabel(command.period),
432
455
  usage,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Hooks, type PluginInput } from "@opencode-ai/plugin";
1
+ import { type Hooks, type PluginInput } from '@opencode-ai/plugin';
2
2
  export declare function QuotaSidebarPlugin(input: PluginInput): Promise<Hooks>;
3
3
  export default QuotaSidebarPlugin;
4
- export type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, QuotaStatus, SessionState, CachedSessionUsage, CachedProviderUsage, IncrementalCursor, } from "./types.js";
5
- export type { UsageSummary } from "./usage.js";
4
+ export type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, QuotaStatus, SessionState, CachedSessionUsage, CachedProviderUsage, IncrementalCursor, } from './types.js';
5
+ export type { UsageSummary } from './usage.js';
package/dist/index.js CHANGED
@@ -1,19 +1,19 @@
1
- import { renderHistoryMarkdownReport, renderMarkdownReport, resolveTitleView, renderSidebarTitle, renderToastMessage, } from "./format.js";
2
- import { createQuotaRuntime } from "./quota.js";
3
- import { authFilePath, dateKeyFromTimestamp, deleteSessionFromDayChunk, evictOldSessions, loadConfig, loadState, normalizeTimestampMs, quotaConfigPaths, resolveOpencodeDataDir, saveState, stateFilePath, } from "./storage.js";
4
- import { debug, swallow } from "./helpers.js";
5
- import { normalizeBaseTitle } from "./title.js";
6
- import { createDescendantsResolver } from "./descendants.js";
7
- import { createTitleRefreshScheduler } from "./title_refresh.js";
8
- import { createQuotaSidebarTools } from "./tools.js";
9
- import { createEventDispatcher } from "./events.js";
10
- import { createPersistenceScheduler } from "./persistence.js";
11
- import { createQuotaService } from "./quota_service.js";
12
- import { createUsageService } from "./usage_service.js";
13
- import { createTitleApplicator } from "./title_apply.js";
14
- import { listCurrentProviderIDs } from "./provider_catalog.js";
15
- const SHUTDOWN_HOOK_KEY = Symbol.for("opencode-quota-sidebar.shutdown-hook");
16
- const SHUTDOWN_CALLBACKS_KEY = Symbol.for("opencode-quota-sidebar.shutdown-callbacks");
1
+ import { renderHistoryMarkdownReport, renderMarkdownReport, resolveTitleView, renderSidebarTitle, renderToastMessage, } from './format.js';
2
+ import { createQuotaRuntime } from './quota.js';
3
+ import { authFilePath, dateKeyFromTimestamp, deleteSessionFromDayChunk, evictOldSessions, loadConfig, loadState, normalizeTimestampMs, quotaConfigPaths, resolveOpencodeDataDir, saveState, stateFilePath, } from './storage.js';
4
+ import { debug, swallow } from './helpers.js';
5
+ import { normalizeBaseTitle } from './title.js';
6
+ import { createDescendantsResolver } from './descendants.js';
7
+ import { createTitleRefreshScheduler } from './title_refresh.js';
8
+ import { createQuotaSidebarTools } from './tools.js';
9
+ import { createEventDispatcher } from './events.js';
10
+ import { createPersistenceScheduler } from './persistence.js';
11
+ import { createQuotaService } from './quota_service.js';
12
+ import { createUsageService } from './usage_service.js';
13
+ import { createTitleApplicator } from './title_apply.js';
14
+ import { listCurrentProviderIDs } from './provider_catalog.js';
15
+ const SHUTDOWN_HOOK_KEY = Symbol.for('opencode-quota-sidebar.shutdown-hook');
16
+ const SHUTDOWN_CALLBACKS_KEY = Symbol.for('opencode-quota-sidebar.shutdown-callbacks');
17
17
  const SESSION_ACTIVE_GRACE_MS = 15_000;
18
18
  export async function QuotaSidebarPlugin(input) {
19
19
  const quotaRuntime = createQuotaRuntime();
@@ -95,7 +95,7 @@ export async function QuotaSidebarPlugin(input) {
95
95
  query: { directory: input.directory },
96
96
  throwOnError: true,
97
97
  })
98
- .catch(swallow("listSessionChildren"));
98
+ .catch(swallow('listSessionChildren'));
99
99
  return response?.data ?? [];
100
100
  },
101
101
  getParentID: (sessionID) => state.sessions[sessionID]?.parentID,
@@ -121,6 +121,15 @@ export async function QuotaSidebarPlugin(input) {
121
121
  const summarizeSessionUsageForDisplay = usageService.summarizeSessionUsageForDisplay;
122
122
  const summarizeForTool = usageService.summarizeForTool;
123
123
  const summarizeHistoryForTool = usageService.summarizeHistoryUsage;
124
+ const listCurrentProviderIDsSafe = async () => {
125
+ return listCurrentProviderIDs({
126
+ client: input.client,
127
+ directory: input.directory,
128
+ }).catch((error) => {
129
+ debug(`listCurrentProviderIDs failed: ${String(error)}`);
130
+ return undefined;
131
+ });
132
+ };
124
133
  const activeSessionUntil = new Map();
125
134
  let lastTuiSessionID;
126
135
  const markSessionActive = (sessionID, now = Date.now()) => {
@@ -182,6 +191,7 @@ export async function QuotaSidebarPlugin(input) {
182
191
  getTitleView: () => resolveTitleView({ config }),
183
192
  getQuotaSnapshots,
184
193
  summarizeSessionUsageForDisplay,
194
+ listCurrentProviderIDs: listCurrentProviderIDsSafe,
185
195
  scheduleParentRefreshIfSafe,
186
196
  isSessionActive,
187
197
  restoreConcurrency: RESTORE_TITLE_CONCURRENCY,
@@ -192,7 +202,7 @@ export async function QuotaSidebarPlugin(input) {
192
202
  return;
193
203
  await titleApplicator.applyTitle(sessionID);
194
204
  },
195
- onError: swallow("titleRefresh"),
205
+ onError: swallow('titleRefresh'),
196
206
  });
197
207
  scheduleTitleRefresh = titleRefresh.schedule;
198
208
  const startupTitleWork = Promise.resolve();
@@ -200,11 +210,11 @@ export async function QuotaSidebarPlugin(input) {
200
210
  await Promise.race([
201
211
  startupTitleWork,
202
212
  new Promise((resolve) => setTimeout(resolve, 5_000)),
203
- ]).catch(swallow("shutdown:startupTitleWork"));
213
+ ]).catch(swallow('shutdown:startupTitleWork'));
204
214
  await titleRefresh
205
215
  .waitForQuiescence()
206
- .catch(swallow("shutdown:titleQuiescence"));
207
- await flushSave().catch(swallow("shutdown:flushSave"));
216
+ .catch(swallow('shutdown:titleQuiescence'));
217
+ await flushSave().catch(swallow('shutdown:flushSave'));
208
218
  };
209
219
  const processWithHook = process;
210
220
  const shutdownCallbacks = (processWithHook[SHUTDOWN_CALLBACKS_KEY] ||=
@@ -212,10 +222,10 @@ export async function QuotaSidebarPlugin(input) {
212
222
  shutdownCallbacks.add(shutdown);
213
223
  if (!processWithHook[SHUTDOWN_HOOK_KEY]) {
214
224
  processWithHook[SHUTDOWN_HOOK_KEY] = true;
215
- process.once("beforeExit", () => {
225
+ process.once('beforeExit', () => {
216
226
  void Promise.allSettled(Array.from(shutdownCallbacks).map((callback) => callback()));
217
227
  });
218
- for (const signal of ["SIGINT", "SIGTERM"]) {
228
+ for (const signal of ['SIGINT', 'SIGTERM']) {
219
229
  process.once(signal, () => {
220
230
  void Promise.allSettled(Array.from(shutdownCallbacks).map((callback) => callback())).finally(() => {
221
231
  process.kill(process.pid, signal);
@@ -232,12 +242,12 @@ export async function QuotaSidebarPlugin(input) {
232
242
  body: {
233
243
  title: `Quota ${period}`,
234
244
  message,
235
- variant: "info",
245
+ variant: 'info',
236
246
  duration: config.toast.durationMs,
237
247
  },
238
248
  throwOnError: true,
239
249
  })
240
- .catch(swallow("showToast"));
250
+ .catch(swallow('showToast'));
241
251
  };
242
252
  const expiryAlertText = (iso, nowMs = Date.now()) => {
243
253
  if (!iso)
@@ -251,7 +261,7 @@ export async function QuotaSidebarPlugin(input) {
251
261
  return undefined;
252
262
  const value = new Date(timestamp);
253
263
  const now = new Date(nowMs);
254
- const two = (num) => `${num}`.padStart(2, "0");
264
+ const two = (num) => `${num}`.padStart(2, '0');
255
265
  const hhmm = `${two(value.getHours())}:${two(value.getMinutes())}`;
256
266
  const sameDay = value.getFullYear() === now.getFullYear() &&
257
267
  value.getMonth() === now.getMonth() &&
@@ -270,10 +280,13 @@ export async function QuotaSidebarPlugin(input) {
270
280
  }
271
281
  expiryToastInflight.add(sessionID);
272
282
  try {
273
- const quotas = await getQuotaSnapshots([], { allowDefault: true });
283
+ const allowedProviderIDs = await listCurrentProviderIDsSafe();
284
+ if (!allowedProviderIDs || allowedProviderIDs.size === 0)
285
+ return;
286
+ const quotas = await getQuotaSnapshots([...allowedProviderIDs]);
274
287
  const nowMs = Date.now();
275
288
  const expiryLines = quotas
276
- .filter((item) => item.status === "ok")
289
+ .filter((item) => item.status === 'ok')
277
290
  .map((item) => ({
278
291
  label: item.shortLabel || item.label,
279
292
  value: expiryAlertText(item.expiresAt, nowMs),
@@ -288,10 +301,10 @@ export async function QuotaSidebarPlugin(input) {
288
301
  markDirty(dateKey);
289
302
  scheduleSave();
290
303
  const body = [
291
- "Expiry Soon",
304
+ 'Expiry Soon',
292
305
  ...expiryLines.map((item) => `${item.label} ${item.value}`),
293
- ].join("\n");
294
- await showToast("session", body);
306
+ ].join('\n');
307
+ await showToast('session', body);
295
308
  }
296
309
  catch (error) {
297
310
  debug(`expiry toast check failed: ${String(error)}`);
@@ -329,7 +342,7 @@ export async function QuotaSidebarPlugin(input) {
329
342
  });
330
343
  },
331
344
  onSessionDeleted: async (session) => {
332
- await flushSave().catch(swallow("onSessionDeleted:flushSave"));
345
+ await flushSave().catch(swallow('onSessionDeleted:flushSave'));
333
346
  descendantsResolver.invalidateForAncestors(session.parentID);
334
347
  descendantsResolver.invalidateForAncestors(session.id);
335
348
  usageService.forgetSession(session.id);
@@ -343,7 +356,7 @@ export async function QuotaSidebarPlugin(input) {
343
356
  delete state.sessionDateMap[session.id];
344
357
  markDirty(dateKey);
345
358
  scheduleSave();
346
- const deletedFromChunk = await deleteSessionFromDayChunk(statePath, session.id, dateKey).catch(swallow("deleteSessionFromDayChunk"));
359
+ const deletedFromChunk = await deleteSessionFromDayChunk(statePath, session.id, dateKey).catch(swallow('deleteSessionFromDayChunk'));
347
360
  if (deletedFromChunk) {
348
361
  delete state.deletedSessionDateMap[session.id];
349
362
  scheduleSave();
@@ -372,9 +385,9 @@ export async function QuotaSidebarPlugin(input) {
372
385
  onAssistantMessageUpdated: async (message) => {
373
386
  const now = Date.now();
374
387
  const completed = message.time.completed;
375
- if (typeof completed !== "number" || !Number.isFinite(completed)) {
388
+ if (typeof completed !== 'number' || !Number.isFinite(completed)) {
376
389
  const created = message.time.created;
377
- if (typeof created === "number" &&
390
+ if (typeof created === 'number' &&
378
391
  Number.isFinite(created) &&
379
392
  created < now - SESSION_ACTIVE_GRACE_MS) {
380
393
  return;
@@ -1,11 +1,11 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
- import type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, SessionState } from "./types.js";
3
- import type { UsageSummary } from "./usage.js";
4
- import { type TitleView } from "./format.js";
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ import type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, SessionState } from './types.js';
3
+ import type { UsageSummary } from './usage.js';
4
+ import { type TitleView } from './format.js';
5
5
  export declare function createTitleApplicator(deps: {
6
6
  state: QuotaSidebarState;
7
7
  config: QuotaSidebarConfig;
8
- client: PluginInput["client"];
8
+ client: PluginInput['client'];
9
9
  directory: string;
10
10
  ensureSessionState: (sessionID: string, title: string, createdAt: number, parentID?: string | null) => SessionState;
11
11
  markDirty: (dateKey: string | undefined) => void;
@@ -16,6 +16,7 @@ export declare function createTitleApplicator(deps: {
16
16
  allowDefault?: boolean;
17
17
  }) => Promise<QuotaSnapshot[]>;
18
18
  summarizeSessionUsageForDisplay: (sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
19
+ listCurrentProviderIDs?: () => Promise<Set<string> | undefined>;
19
20
  scheduleParentRefreshIfSafe: (sessionID: string, parentID?: string) => void;
20
21
  isSessionActive?: (sessionID: string) => boolean;
21
22
  restoreConcurrency: number;
@@ -1,8 +1,8 @@
1
- import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from "./title.js";
2
- import { toCachedSessionUsage } from "./usage.js";
3
- import { swallow, debug } from "./helpers.js";
4
- import { resolveTitleView, selectDesktopCompactProviderIDs, } from "./format.js";
5
- import { collapseQuotaSnapshots } from "./quota_render.js";
1
+ import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from './title.js';
2
+ import { toCachedSessionUsage } from './usage.js';
3
+ import { swallow, debug } from './helpers.js';
4
+ import { resolveTitleView, selectDesktopCompactProviderIDs, } from './format.js';
5
+ import { collapseQuotaSnapshots } from './quota_render.js';
6
6
  export function createTitleApplicator(deps) {
7
7
  const pendingAppliedTitle = new Map();
8
8
  const recentRestore = new Map();
@@ -25,6 +25,11 @@ export function createTitleApplicator(deps) {
25
25
  }
26
26
  return true;
27
27
  };
28
+ const filterAllowedProviderIDs = (providerIDs, allowedProviderIDs) => {
29
+ if (!allowedProviderIDs)
30
+ return [];
31
+ return providerIDs.filter((providerID) => allowedProviderIDs.has(providerID));
32
+ };
28
33
  const applyTitle = async (sessionID) => {
29
34
  if (!deps.config.sidebar.enabled)
30
35
  return false;
@@ -39,13 +44,13 @@ export function createTitleApplicator(deps) {
39
44
  query: { directory: deps.directory },
40
45
  throwOnError: true,
41
46
  })
42
- .catch(swallow("applyTitle:getSession"));
47
+ .catch(swallow('applyTitle:getSession'));
43
48
  if (!session)
44
49
  return false;
45
50
  if (!session.data ||
46
- typeof session.data.title !== "string" ||
51
+ typeof session.data.title !== 'string' ||
47
52
  !session.data.time ||
48
- typeof session.data.time.created !== "number") {
53
+ typeof session.data.time.created !== 'number') {
49
54
  debug(`applyTitle skipped malformed session payload for ${sessionID}`);
50
55
  return false;
51
56
  }
@@ -53,7 +58,7 @@ export function createTitleApplicator(deps) {
53
58
  // Detect whether the current title is our own decorated form.
54
59
  const currentTitle = session.data.title;
55
60
  if (canonicalizeTitle(currentTitle) !==
56
- canonicalizeTitle(sessionState.lastAppliedTitle || "")) {
61
+ canonicalizeTitle(sessionState.lastAppliedTitle || '')) {
57
62
  if (looksDecorated(currentTitle)) {
58
63
  if (/\r?\n/.test(currentTitle)) {
59
64
  const normalizedBase = normalizeBaseTitle(currentTitle);
@@ -97,10 +102,16 @@ export function createTitleApplicator(deps) {
97
102
  const usage = await deps.summarizeSessionUsageForDisplay(sessionID, deps.config.sidebar.includeChildren);
98
103
  const view = deps.getTitleView?.(sessionID) ??
99
104
  resolveTitleView({ config: deps.config });
100
- const panelQuotaProviders = Array.from(new Set(Object.keys(usage.providers)));
101
- const quotaProviders = Array.from(new Set(view === "compact"
105
+ const rawPanelQuotaProviders = Array.from(new Set(Object.keys(usage.providers)));
106
+ const rawQuotaProviders = Array.from(new Set(view === 'compact'
102
107
  ? selectDesktopCompactProviderIDs(usage, deps.config)
103
- : panelQuotaProviders));
108
+ : rawPanelQuotaProviders));
109
+ const allowedProviderIDs = deps.config.sidebar.showQuota &&
110
+ (rawPanelQuotaProviders.length > 0 || rawQuotaProviders.length > 0)
111
+ ? await deps.listCurrentProviderIDs?.()
112
+ : undefined;
113
+ const panelQuotaProviders = filterAllowedProviderIDs(rawPanelQuotaProviders, allowedProviderIDs);
114
+ const quotaProviders = filterAllowedProviderIDs(rawQuotaProviders, allowedProviderIDs);
104
115
  const quotas = deps.config.sidebar.showQuota && quotaProviders.length > 0
105
116
  ? await deps.getQuotaSnapshots(quotaProviders)
106
117
  : [];
@@ -157,7 +168,7 @@ export function createTitleApplicator(deps) {
157
168
  body: { title: nextTitle },
158
169
  throwOnError: true,
159
170
  })
160
- .catch(swallow("applyTitle:update"));
171
+ .catch(swallow('applyTitle:update'));
161
172
  if (!updated) {
162
173
  pendingAppliedTitle.delete(sessionID);
163
174
  sessionState.lastAppliedTitle = previousApplied;
@@ -192,7 +203,7 @@ export function createTitleApplicator(deps) {
192
203
  // of our own update. Extract the base title from line 1 instead of
193
204
  // treating the whole decorated string as the new base title.
194
205
  if (canonicalizeTitleForCompare(args.incomingTitle) ===
195
- canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle || "")) {
206
+ canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle || '')) {
196
207
  return;
197
208
  }
198
209
  if (looksDecorated(args.incomingTitle) &&
@@ -236,18 +247,18 @@ export function createTitleApplicator(deps) {
236
247
  query: { directory: deps.directory },
237
248
  throwOnError: true,
238
249
  })
239
- .catch(swallow("restoreSessionTitle:get"));
250
+ .catch(swallow('restoreSessionTitle:get'));
240
251
  if (!session)
241
252
  return false;
242
253
  if (!session.data ||
243
- typeof session.data.title !== "string" ||
254
+ typeof session.data.title !== 'string' ||
244
255
  !session.data.time ||
245
- typeof session.data.time.created !== "number") {
256
+ typeof session.data.time.created !== 'number') {
246
257
  debug(`restoreSessionTitle skipped malformed session payload for ${sessionID}`);
247
258
  return false;
248
259
  }
249
260
  const sessionState = deps.ensureSessionState(sessionID, session.data.title, session.data.time.created, session.data.parentID ?? null);
250
- const baseTitle = canonicalizeTitle(sessionState.baseTitle) || "Session";
261
+ const baseTitle = canonicalizeTitle(sessionState.baseTitle) || 'Session';
251
262
  if (session.data.title === baseTitle) {
252
263
  if (sessionState.lastAppliedTitle !== undefined) {
253
264
  sessionState.lastAppliedTitle = undefined;
@@ -265,7 +276,7 @@ export function createTitleApplicator(deps) {
265
276
  body: { title: baseTitle },
266
277
  throwOnError: true,
267
278
  })
268
- .catch(swallow("restoreSessionTitle:update"));
279
+ .catch(swallow('restoreSessionTitle:update'));
269
280
  if (!updated)
270
281
  return false;
271
282
  pendingAppliedTitle.delete(sessionID);
package/dist/tools.d.ts CHANGED
@@ -22,7 +22,7 @@ export declare function createQuotaSidebarTools(deps: {
22
22
  showToast: (period: 'session' | 'day' | 'week' | 'month' | 'toggle', message: string) => Promise<void>;
23
23
  summarizeForTool: (period: 'session' | 'day' | 'week' | 'month', sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
24
24
  summarizeHistoryForTool: (period: HistoryPeriod, since: string) => Promise<HistoryUsageResult>;
25
- listCurrentProviderIDs?: () => Promise<Set<string>>;
25
+ listCurrentProviderIDs: () => Promise<Set<string>>;
26
26
  getQuotaSnapshots: (providerIDs: string[], options?: {
27
27
  allowDefault?: boolean;
28
28
  }) => Promise<QuotaSnapshot[]>;
package/dist/tools.js CHANGED
@@ -1,6 +1,30 @@
1
1
  import * as z from 'zod';
2
2
  import { sinceFromLast } from './period.js';
3
3
  import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, } from './provider_catalog.js';
4
+ function emptyProviderUsage(usage) {
5
+ return {
6
+ ...usage,
7
+ providers: {},
8
+ };
9
+ }
10
+ function strictFilterUsageProviders(usage, allowedProviderIDs) {
11
+ if (allowedProviderIDs.size === 0)
12
+ return emptyProviderUsage(usage);
13
+ return filterUsageProvidersForDisplay(usage, allowedProviderIDs);
14
+ }
15
+ function strictFilterHistoryProviders(history, allowedProviderIDs) {
16
+ if (allowedProviderIDs.size === 0) {
17
+ return {
18
+ ...history,
19
+ rows: history.rows.map((row) => ({
20
+ ...row,
21
+ usage: emptyProviderUsage(row.usage),
22
+ })),
23
+ total: emptyProviderUsage(history.total),
24
+ };
25
+ }
26
+ return filterHistoryProvidersForDisplay(history, allowedProviderIDs);
27
+ }
4
28
  function tool(input) {
5
29
  return input;
6
30
  }
@@ -51,17 +75,11 @@ export function createQuotaSidebarTools(deps) {
51
75
  (period !== 'session' && last !== undefined
52
76
  ? sinceFromLast(period, last)
53
77
  : undefined);
54
- const allowedProviderIDs = await deps
55
- .listCurrentProviderIDs?.()
56
- .catch(() => new Set());
78
+ const allowedProviderIDs = await deps.listCurrentProviderIDs();
57
79
  if (period !== 'session' && resolvedSince) {
58
80
  const historyRaw = await deps.summarizeHistoryForTool(period, resolvedSince);
59
- const history = allowedProviderIDs
60
- ? filterHistoryProvidersForDisplay(historyRaw, allowedProviderIDs)
61
- : historyRaw;
62
- const quotas = await deps.getQuotaSnapshots([], {
63
- allowDefault: true,
64
- });
81
+ const history = strictFilterHistoryProviders(historyRaw, allowedProviderIDs);
82
+ const quotas = await deps.getQuotaSnapshots(Object.keys(history.total.providers));
65
83
  const markdown = deps.renderHistoryMarkdownReport(history, quotas, {
66
84
  showCost: deps.config.sidebar.showCost,
67
85
  });
@@ -77,12 +95,8 @@ export function createQuotaSidebarTools(deps) {
77
95
  ? (args.includeChildren ?? deps.config.sidebar.includeChildren)
78
96
  : false;
79
97
  const usageRaw = await deps.summarizeForTool(period, context.sessionID, includeChildren);
80
- const usage = allowedProviderIDs
81
- ? filterUsageProvidersForDisplay(usageRaw, allowedProviderIDs)
82
- : usageRaw;
83
- // For quota_summary, always show all subscription quota balances,
84
- // regardless of which providers were used in the session.
85
- const quotas = await deps.getQuotaSnapshots([], { allowDefault: true });
98
+ const usage = strictFilterUsageProviders(usageRaw, allowedProviderIDs);
99
+ const quotas = await deps.getQuotaSnapshots(Object.keys(usage.providers));
86
100
  const markdown = deps.renderMarkdownReport(period, usage, quotas, {
87
101
  showCost: deps.config.sidebar.showCost,
88
102
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "4.0.11",
3
+ "version": "4.0.13",
4
4
  "description": "OpenCode plugin that shows quota and token usage in TUI sidebar panels and compact session titles",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",