@leo000001/opencode-quota-sidebar 3.0.6 → 3.0.10

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/index.js CHANGED
@@ -178,6 +178,7 @@ export async function QuotaSidebarPlugin(input) {
178
178
  getQuotaSnapshots,
179
179
  summarizeSessionUsageForDisplay,
180
180
  scheduleParentRefreshIfSafe,
181
+ isSessionActive,
181
182
  restoreConcurrency: RESTORE_TITLE_CONCURRENCY,
182
183
  });
183
184
  const titleRefresh = createTitleRefreshScheduler({
@@ -189,30 +190,7 @@ export async function QuotaSidebarPlugin(input) {
189
190
  onError: swallow('titleRefresh'),
190
191
  });
191
192
  scheduleTitleRefresh = titleRefresh.schedule;
192
- const restoreAllVisibleTitles = titleApplicator.restoreAllVisibleTitles;
193
- const refreshAllTouchedTitles = titleApplicator.refreshAllTouchedTitles;
194
- const refreshAllVisibleTitles = titleApplicator.refreshAllVisibleTitles;
195
- let startupTitleWork = Promise.resolve();
196
- const runStartupRestore = async (attempt = 0) => {
197
- const result = await restoreAllVisibleTitles({
198
- abortIfEnabled: config.sidebar.enabled,
199
- });
200
- if (result.restored === result.attempted)
201
- return;
202
- debug(`startup restore incomplete: restored ${result.restored}/${result.attempted} touched titles while display mode remains OFF`);
203
- if (state.titleEnabled || config.sidebar.enabled === false)
204
- return;
205
- if (attempt >= 2)
206
- return;
207
- await new Promise((resolve) => setTimeout(resolve, 1_000));
208
- await runStartupRestore(attempt + 1);
209
- };
210
- if (!state.titleEnabled || !config.sidebar.enabled) {
211
- startupTitleWork = runStartupRestore().catch(swallow('startup:restoreAllVisibleTitles'));
212
- }
213
- else {
214
- startupTitleWork = Promise.resolve();
215
- }
193
+ const startupTitleWork = Promise.resolve();
216
194
  const shutdown = async () => {
217
195
  await Promise.race([
218
196
  startupTitleWork,
@@ -379,11 +357,17 @@ export async function QuotaSidebarPlugin(input) {
379
357
  scheduleParentRefreshIfSafe(info.sessionID, state.sessions[info.sessionID]?.parentID);
380
358
  },
381
359
  onAssistantMessageUpdated: async (message) => {
382
- markSessionActive(message.sessionID);
360
+ const now = Date.now();
383
361
  const completed = message.time.completed;
384
362
  if (typeof completed !== 'number' || !Number.isFinite(completed)) {
363
+ markSessionActive(message.sessionID, now);
364
+ return;
365
+ }
366
+ const wasActive = isSessionActive(message.sessionID, now);
367
+ if (!wasActive) {
385
368
  return;
386
369
  }
370
+ markSessionActive(message.sessionID, now);
387
371
  usageService.markSessionDirty(message.sessionID);
388
372
  scheduleActiveTitleRefresh(message.sessionID);
389
373
  void maybeShowExpiryToast(message.sessionID);
@@ -406,14 +390,13 @@ export async function QuotaSidebarPlugin(input) {
406
390
  scheduleSave,
407
391
  flushSave,
408
392
  waitForStartupTitleWork: () => startupTitleWork,
393
+ markSessionActive,
409
394
  refreshSessionTitle: (sessionID, delay) => scheduleActiveTitleRefresh(sessionID, delay ?? 250),
410
395
  cancelAllTitleRefreshes: () => titleRefresh.cancelAll(),
411
396
  flushScheduledTitleRefreshes: () => titleRefresh.flushScheduled(),
412
397
  waitForTitleRefreshIdle: () => titleRefresh.waitForIdle(),
413
398
  waitForTitleRefreshQuiescence: () => titleRefresh.waitForQuiescence(),
414
- restoreAllVisibleTitles,
415
- refreshAllTouchedTitles,
416
- refreshAllVisibleTitles,
399
+ restoreSessionTitle: (sessionID) => titleApplicator.restoreSessionTitle(sessionID),
417
400
  showToast,
418
401
  summarizeForTool,
419
402
  getQuotaSnapshots,
@@ -1,5 +1,60 @@
1
1
  import { debug, debugError, isRecord, swallow } from '../../helpers.js';
2
- import { OPENAI_OAUTH_CLIENT_ID, configuredProviderEnabled, fetchWithTimeout, normalizePercent, parseRateLimitWindow, toIso, } from '../common.js';
2
+ import { OPENAI_OAUTH_CLIENT_ID, asNumber, configuredProviderEnabled, fetchWithTimeout, toIso, windowLabel, } from '../common.js';
3
+ function decodeJwtPayload(token) {
4
+ try {
5
+ const parts = token.split('.');
6
+ if (parts.length !== 3)
7
+ return undefined;
8
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
9
+ return isRecord(payload) ? payload : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ function extractAccountIdFromJwt(token) {
16
+ const payload = decodeJwtPayload(token);
17
+ if (!payload)
18
+ return undefined;
19
+ const authClaim = payload['https://api.openai.com/auth'];
20
+ if (!isRecord(authClaim))
21
+ return undefined;
22
+ const accountID = authClaim.chatgpt_account_id;
23
+ return typeof accountID === 'string' && accountID ? accountID : undefined;
24
+ }
25
+ function normalizeOpenAIQuotaPercent(value) {
26
+ const numeric = asNumber(value);
27
+ if (numeric === undefined || Number.isNaN(numeric))
28
+ return undefined;
29
+ const expanded = numeric > 0 && numeric < 1 ? numeric * 100 : numeric;
30
+ if (expanded < 0)
31
+ return 0;
32
+ if (expanded > 100)
33
+ return 100;
34
+ return expanded;
35
+ }
36
+ function windowResetAt(win, fallback) {
37
+ const absolute = toIso(win.reset_at ?? fallback?.reset_at);
38
+ if (absolute)
39
+ return absolute;
40
+ const resetAfterSeconds = asNumber(win.reset_after_seconds) ?? asNumber(fallback?.reset_after_seconds);
41
+ if (resetAfterSeconds === undefined || resetAfterSeconds < 0)
42
+ return undefined;
43
+ return new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
44
+ }
45
+ function parseOpenAIWindow(win, fallbackLabel) {
46
+ const usedPercent = normalizeOpenAIQuotaPercent(win.used_percent);
47
+ const remainingPercent = normalizeOpenAIQuotaPercent(win.remaining_percent) ??
48
+ (usedPercent === undefined ? undefined : 100 - usedPercent);
49
+ if (remainingPercent === undefined)
50
+ return undefined;
51
+ return {
52
+ label: windowLabel(win, fallbackLabel),
53
+ remainingPercent,
54
+ usedPercent,
55
+ resetAt: windowResetAt(win),
56
+ };
57
+ }
3
58
  async function fetchOpenAIQuota(ctx) {
4
59
  const checkedAt = Date.now();
5
60
  const base = {
@@ -85,13 +140,15 @@ async function fetchOpenAIQuota(ctx) {
85
140
  }
86
141
  }
87
142
  }
143
+ const accountId = (typeof ctx.auth.accountId === 'string' && ctx.auth.accountId) ||
144
+ extractAccountIdFromJwt(access);
88
145
  const headers = new Headers({
89
146
  Authorization: `Bearer ${access}`,
90
147
  Accept: 'application/json',
91
148
  'User-Agent': 'opencode-quota-sidebar',
92
149
  });
93
- if (typeof ctx.auth.accountId === 'string' && ctx.auth.accountId) {
94
- headers.set('ChatGPT-Account-Id', ctx.auth.accountId);
150
+ if (accountId) {
151
+ headers.set('ChatGPT-Account-Id', accountId);
95
152
  }
96
153
  const response = await fetchWithTimeout('https://chatgpt.com/backend-api/wham/usage', { headers }, ctx.config.quota.requestTimeoutMs).catch(swallow('fetchOpenAIQuota:usage'));
97
154
  if (!response) {
@@ -123,18 +180,18 @@ async function fetchOpenAIQuota(ctx) {
123
180
  const primary = isRecord(rateLimit.primary_window)
124
181
  ? rateLimit.primary_window
125
182
  : {};
126
- const usedPercent = normalizePercent(primary.used_percent);
127
- const remainingPercent = normalizePercent(primary.remaining_percent) ??
183
+ const usedPercent = normalizeOpenAIQuotaPercent(primary.used_percent);
184
+ const remainingPercent = normalizeOpenAIQuotaPercent(primary.remaining_percent) ??
128
185
  (usedPercent === undefined ? undefined : 100 - usedPercent);
129
- const resetAt = toIso(primary.reset_at ?? rateLimit.reset_at);
186
+ const resetAt = windowResetAt(primary, rateLimit);
130
187
  const windows = [];
131
188
  if (remainingPercent !== undefined) {
132
- const primaryWin = parseRateLimitWindow(primary, '');
189
+ const primaryWin = parseOpenAIWindow(primary, '');
133
190
  if (primaryWin)
134
191
  windows.push(primaryWin);
135
192
  }
136
193
  if (isRecord(rateLimit.secondary_window)) {
137
- const secondaryWin = parseRateLimitWindow(rateLimit.secondary_window, 'Weekly');
194
+ const secondaryWin = parseOpenAIWindow(rateLimit.secondary_window, 'Weekly');
138
195
  if (secondaryWin)
139
196
  windows.push(secondaryWin);
140
197
  }
@@ -17,6 +17,7 @@ export declare function createTitleApplicator(deps: {
17
17
  }) => Promise<QuotaSnapshot[]>;
18
18
  summarizeSessionUsageForDisplay: (sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
19
19
  scheduleParentRefreshIfSafe: (sessionID: string, parentID?: string) => void;
20
+ isSessionActive?: (sessionID: string) => boolean;
20
21
  restoreConcurrency: number;
21
22
  }): {
22
23
  applyTitle: (sessionID: string) => Promise<boolean>;
@@ -28,6 +28,10 @@ export function createTitleApplicator(deps) {
28
28
  const applyTitle = async (sessionID) => {
29
29
  if (!deps.config.sidebar.enabled)
30
30
  return false;
31
+ if (deps.isSessionActive && !deps.isSessionActive(sessionID)) {
32
+ debug(`applyTitle skipped inactive session ${sessionID}`);
33
+ return false;
34
+ }
31
35
  let stateMutated = false;
32
36
  const session = await deps.client.session
33
37
  .get({
package/dist/tools.d.ts CHANGED
@@ -10,26 +10,13 @@ export declare function createQuotaSidebarTools(deps: {
10
10
  scheduleSave: () => void;
11
11
  flushSave: () => Promise<void>;
12
12
  waitForStartupTitleWork: () => Promise<void>;
13
+ markSessionActive?: (sessionID: string) => void;
13
14
  refreshSessionTitle: (sessionID: string, delay?: number) => void;
14
15
  cancelAllTitleRefreshes: () => void;
15
16
  flushScheduledTitleRefreshes: () => Promise<void>;
16
17
  waitForTitleRefreshIdle: () => Promise<void>;
17
18
  waitForTitleRefreshQuiescence: () => Promise<void>;
18
- restoreAllVisibleTitles: () => Promise<{
19
- attempted: number;
20
- restored: number;
21
- listFailed: boolean;
22
- }>;
23
- refreshAllTouchedTitles: () => Promise<{
24
- attempted: number;
25
- refreshed: number;
26
- listFailed: boolean;
27
- }>;
28
- refreshAllVisibleTitles: () => Promise<{
29
- attempted: number;
30
- refreshed: number;
31
- listFailed: boolean;
32
- }>;
19
+ restoreSessionTitle?: (sessionID: string) => Promise<boolean>;
33
20
  showToast: (period: 'session' | 'day' | 'week' | 'month' | 'toggle', message: string) => Promise<void>;
34
21
  summarizeForTool: (period: 'session' | 'day' | 'week' | 'month', sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
35
22
  getQuotaSnapshots: (providerIDs: string[], options?: {
package/dist/tools.js CHANGED
@@ -63,11 +63,13 @@ export function createQuotaSidebarTools(deps) {
63
63
  deps.setTitleEnabled(true);
64
64
  deps.scheduleSave();
65
65
  await deps.flushSave();
66
+ deps.markSessionActive?.(context.sessionID);
66
67
  deps.refreshSessionTitle(context.sessionID, 0);
67
68
  if (startupTimedOut) {
68
69
  void deps.waitForStartupTitleWork().then(() => {
69
70
  if (!deps.getTitleEnabled())
70
71
  return;
72
+ deps.markSessionActive?.(context.sessionID);
71
73
  deps.refreshSessionTitle(context.sessionID, 0);
72
74
  });
73
75
  }
@@ -79,14 +81,17 @@ export function createQuotaSidebarTools(deps) {
79
81
  await deps.flushSave();
80
82
  deps.cancelAllTitleRefreshes();
81
83
  await deps.waitForTitleRefreshQuiescence();
82
- const restore = await deps.restoreAllVisibleTitles();
83
- if (restore.restored === restore.attempted) {
84
+ const restoredCurrent = await (deps.restoreSessionTitle
85
+ ? deps.restoreSessionTitle(context.sessionID)
86
+ : Promise.resolve(false));
87
+ if (restoredCurrent) {
84
88
  await deps.showToast('toggle', 'Sidebar usage display: OFF');
85
- return 'Sidebar usage display is now OFF. Touched session titles were restored to base titles.';
89
+ return 'Sidebar usage display is now OFF. The current session title was restored to its base title.';
86
90
  }
87
91
  deps.setTitleEnabled(true);
88
92
  deps.scheduleSave();
89
93
  await deps.flushSave();
94
+ deps.markSessionActive?.(context.sessionID);
90
95
  deps.refreshSessionTitle(context.sessionID, 0);
91
96
  await deps.showToast('toggle', 'Sidebar usage display: OFF failed');
92
97
  return 'Sidebar usage display remains ON because some touched session titles could not be restored. Try again after the session service recovers.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "3.0.6",
3
+ "version": "3.0.10",
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",