@leo000001/opencode-quota-sidebar 4.0.9 → 4.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import { isDateKey } from './storage_dates.js';
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { isDateKey } from "./storage_dates.js";
4
4
  /**
5
5
  * Resolve the OpenCode data directory.
6
6
  *
@@ -17,8 +17,8 @@ export function resolveOpencodeDataDir() {
17
17
  return path.resolve(override);
18
18
  const xdg = process.env.XDG_DATA_HOME?.trim();
19
19
  if (xdg)
20
- return path.join(path.resolve(xdg), 'opencode');
21
- return path.join(os.homedir(), '.local', 'share', 'opencode');
20
+ return path.join(path.resolve(xdg), "opencode");
21
+ return path.join(os.homedir(), ".local", "share", "opencode");
22
22
  }
23
23
  /**
24
24
  * Resolve OpenCode config directory.
@@ -31,23 +31,38 @@ export function resolveOpencodeConfigDir() {
31
31
  return path.resolve(override);
32
32
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
33
33
  if (xdg)
34
- return path.join(path.resolve(xdg), 'opencode');
35
- return path.join(os.homedir(), '.config', 'opencode');
34
+ return path.join(path.resolve(xdg), "opencode");
35
+ return path.join(os.homedir(), ".config", "opencode");
36
+ }
37
+ export function opencodeConfigPaths(worktree, directory) {
38
+ const configDir = resolveOpencodeConfigDir();
39
+ return [
40
+ path.join(configDir, "opencode.jsonc"),
41
+ path.join(configDir, "opencode.json"),
42
+ path.join(worktree, "opencode.jsonc"),
43
+ path.join(worktree, "opencode.json"),
44
+ path.join(directory, "opencode.jsonc"),
45
+ path.join(directory, "opencode.json"),
46
+ path.join(worktree, ".opencode", "opencode.jsonc"),
47
+ path.join(worktree, ".opencode", "opencode.json"),
48
+ path.join(directory, ".opencode", "opencode.jsonc"),
49
+ path.join(directory, ".opencode", "opencode.json"),
50
+ ];
36
51
  }
37
52
  export function stateFilePath(dataDir) {
38
- return path.join(dataDir, 'quota-sidebar.state.json');
53
+ return path.join(dataDir, "quota-sidebar.state.json");
39
54
  }
40
55
  export function authFilePath(dataDir) {
41
- return path.join(dataDir, 'auth.json');
56
+ return path.join(dataDir, "auth.json");
42
57
  }
43
58
  export function chunkRootPathFromStateFile(statePath) {
44
- return path.join(path.dirname(statePath), 'quota-sidebar-sessions');
59
+ return path.join(path.dirname(statePath), "quota-sidebar-sessions");
45
60
  }
46
61
  export function chunkFilePath(rootPath, dateKey) {
47
62
  // Defense-in-depth: ensure we never build paths from untrusted inputs.
48
63
  if (!isDateKey(dateKey)) {
49
64
  throw new Error(`invalid dateKey: ${dateKey}`);
50
65
  }
51
- const [year, month, day] = dateKey.split('-');
66
+ const [year, month, day] = dateKey.split("-");
52
67
  return path.join(rootPath, year, month, `${day}.json`);
53
68
  }
@@ -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;
@@ -30,22 +31,5 @@ export declare function createTitleApplicator(deps: {
30
31
  restoreSessionTitle: (sessionID: string, options?: {
31
32
  abortIfEnabled?: boolean;
32
33
  }) => Promise<boolean>;
33
- restoreAllVisibleTitles: (options?: {
34
- abortIfEnabled?: boolean;
35
- }) => Promise<{
36
- attempted: number;
37
- restored: number;
38
- listFailed: boolean;
39
- }>;
40
- refreshAllTouchedTitles: () => Promise<{
41
- attempted: number;
42
- refreshed: number;
43
- listFailed: boolean;
44
- }>;
45
- refreshAllVisibleTitles: () => Promise<{
46
- attempted: number;
47
- refreshed: number;
48
- listFailed: boolean;
49
- }>;
50
34
  forgetSession: (sessionID: string) => void;
51
35
  };
@@ -1,6 +1,6 @@
1
1
  import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from './title.js';
2
2
  import { toCachedSessionUsage } from './usage.js';
3
- import { swallow, debug, mapConcurrent } from './helpers.js';
3
+ import { swallow, debug } from './helpers.js';
4
4
  import { resolveTitleView, selectDesktopCompactProviderIDs, } from './format.js';
5
5
  import { collapseQuotaSnapshots } from './quota_render.js';
6
6
  export function createTitleApplicator(deps) {
@@ -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;
@@ -65,7 +70,8 @@ export function createTitleApplicator(deps) {
65
70
  // Ignore decorated echoes as base-title source.
66
71
  // If we previously applied a decorated title, treat this as an
67
72
  // equivalent echo (OpenCode may normalize whitespace) and keep
68
- // lastAppliedTitle in sync so restoreAllVisibleTitles still works.
73
+ // Keep lastAppliedTitle in sync with server echoes so restore logic can
74
+ // still tell whether the current title is ours.
69
75
  if (sessionState.lastAppliedTitle &&
70
76
  looksDecorated(sessionState.lastAppliedTitle)) {
71
77
  if (sessionState.lastAppliedTitle !== currentTitle) {
@@ -96,10 +102,16 @@ export function createTitleApplicator(deps) {
96
102
  const usage = await deps.summarizeSessionUsageForDisplay(sessionID, deps.config.sidebar.includeChildren);
97
103
  const view = deps.getTitleView?.(sessionID) ??
98
104
  resolveTitleView({ config: deps.config });
99
- const panelQuotaProviders = Array.from(new Set(Object.keys(usage.providers)));
100
- 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'
101
107
  ? selectDesktopCompactProviderIDs(usage, deps.config)
102
- : 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);
103
115
  const quotas = deps.config.sidebar.showQuota && quotaProviders.length > 0
104
116
  ? await deps.getQuotaSnapshots(quotaProviders)
105
117
  : [];
@@ -278,53 +290,10 @@ export function createTitleApplicator(deps) {
278
290
  deps.scheduleSave();
279
291
  return true;
280
292
  };
281
- const restoreAllVisibleTitles = async (options) => {
282
- const touched = Object.entries(deps.state.sessions)
283
- .filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
284
- .map(([sessionID]) => sessionID);
285
- const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => restoreSessionTitle(sessionID, options));
286
- return {
287
- attempted: touched.length,
288
- restored: results.filter(Boolean).length,
289
- listFailed: false,
290
- };
291
- };
292
- const refreshAllTouchedTitles = async () => {
293
- const touched = Object.entries(deps.state.sessions)
294
- .filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
295
- .map(([sessionID]) => sessionID);
296
- const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => applyTitle(sessionID));
297
- return {
298
- attempted: touched.length,
299
- refreshed: results.filter(Boolean).length,
300
- listFailed: false,
301
- };
302
- };
303
- const refreshAllVisibleTitles = async () => {
304
- const list = await deps.client.session
305
- .list({
306
- query: { directory: deps.directory },
307
- throwOnError: true,
308
- })
309
- .catch(swallow('refreshAllVisibleTitles:list'));
310
- if (!list?.data || !Array.isArray(list.data)) {
311
- return { attempted: 0, refreshed: 0, listFailed: true };
312
- }
313
- const sessions = list.data.filter((session) => Boolean(session && typeof session.id === 'string'));
314
- const results = await mapConcurrent(sessions, deps.restoreConcurrency, async (session) => applyTitle(session.id));
315
- return {
316
- attempted: sessions.length,
317
- refreshed: results.filter(Boolean).length,
318
- listFailed: false,
319
- };
320
- };
321
293
  return {
322
294
  applyTitle,
323
295
  handleSessionUpdatedTitle,
324
296
  restoreSessionTitle,
325
- restoreAllVisibleTitles,
326
- refreshAllTouchedTitles,
327
- refreshAllVisibleTitles,
328
297
  forgetSession,
329
298
  };
330
299
  }
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([...allowedProviderIDs]);
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([...allowedProviderIDs]);
86
100
  const markdown = deps.renderMarkdownReport(period, usage, quotas, {
87
101
  showCost: deps.config.sidebar.showCost,
88
102
  });
package/dist/tui.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
2
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
3
3
  declare const plugin: TuiPluginModule & {
4
4
  id: string;
5
5
  };