@leo000001/opencode-quota-sidebar 3.0.5 → 3.0.9

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/events.d.ts CHANGED
@@ -9,5 +9,5 @@ export declare function createEventDispatcher(handlers: {
9
9
  sessionID: string;
10
10
  messageID?: string;
11
11
  }) => Promise<void>;
12
- onAssistantMessageCompleted: (message: AssistantMessage) => Promise<void>;
12
+ onAssistantMessageUpdated: (message: AssistantMessage) => Promise<void>;
13
13
  }): (event: Event) => Promise<void>;
package/dist/events.js CHANGED
@@ -16,7 +16,8 @@ export function createEventDispatcher(handlers) {
16
16
  await handlers.onSessionDeleted(event.properties.info);
17
17
  return;
18
18
  }
19
- if (tui.type === 'tui.prompt.append' || tui.type === 'tui.command.execute') {
19
+ if (tui.type === 'tui.prompt.append' ||
20
+ tui.type === 'tui.command.execute') {
20
21
  await handlers.onTuiActivity();
21
22
  return;
22
23
  }
@@ -39,9 +40,6 @@ export function createEventDispatcher(handlers) {
39
40
  return;
40
41
  if (!isAssistantMessage(event.properties.info))
41
42
  return;
42
- const completed = event.properties.info.time.completed;
43
- if (typeof completed !== 'number' || !Number.isFinite(completed))
44
- return;
45
- await handlers.onAssistantMessageCompleted(event.properties.info);
43
+ await handlers.onAssistantMessageUpdated(event.properties.info);
46
44
  };
47
45
  }
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { createUsageService } from './usage_service.js';
13
13
  import { createTitleApplicator } from './title_apply.js';
14
14
  const SHUTDOWN_HOOK_KEY = Symbol.for('opencode-quota-sidebar.shutdown-hook');
15
15
  const SHUTDOWN_CALLBACKS_KEY = Symbol.for('opencode-quota-sidebar.shutdown-callbacks');
16
+ const SESSION_ACTIVE_GRACE_MS = 15_000;
16
17
  export async function QuotaSidebarPlugin(input) {
17
18
  const quotaRuntime = createQuotaRuntime();
18
19
  const config = await loadConfig(quotaConfigPaths(input.worktree, input.directory));
@@ -116,11 +117,33 @@ export async function QuotaSidebarPlugin(input) {
116
117
  });
117
118
  const summarizeSessionUsageForDisplay = usageService.summarizeSessionUsageForDisplay;
118
119
  const summarizeForTool = usageService.summarizeForTool;
120
+ const activeSessionUntil = new Map();
121
+ const markSessionActive = (sessionID, now = Date.now()) => {
122
+ activeSessionUntil.set(sessionID, now + SESSION_ACTIVE_GRACE_MS);
123
+ };
124
+ const clearSessionActivity = (sessionID) => {
125
+ activeSessionUntil.delete(sessionID);
126
+ };
127
+ const isSessionActive = (sessionID, now = Date.now()) => {
128
+ const expiresAt = activeSessionUntil.get(sessionID);
129
+ if (expiresAt === undefined)
130
+ return false;
131
+ if (expiresAt > now)
132
+ return true;
133
+ activeSessionUntil.delete(sessionID);
134
+ return false;
135
+ };
119
136
  // title apply / refresh lifecycle
120
137
  let scheduleTitleRefresh = (sessionID, delay = 250) => {
121
138
  void sessionID;
122
139
  void delay;
123
140
  };
141
+ const scheduleActiveTitleRefresh = (sessionID, delay = 250) => {
142
+ if (!isSessionActive(sessionID))
143
+ return false;
144
+ scheduleTitleRefresh(sessionID, delay);
145
+ return true;
146
+ };
124
147
  const scheduleParentRefreshIfSafe = (sessionID, parentID) => {
125
148
  if (!config.sidebar.includeChildren)
126
149
  return;
@@ -140,7 +163,7 @@ export async function QuotaSidebarPlugin(input) {
140
163
  seen.add(current);
141
164
  current = state.sessions[current]?.parentID;
142
165
  }
143
- scheduleTitleRefresh(parentID, 0);
166
+ scheduleActiveTitleRefresh(parentID, 0);
144
167
  };
145
168
  const titleApplicator = createTitleApplicator({
146
169
  state,
@@ -155,42 +178,19 @@ export async function QuotaSidebarPlugin(input) {
155
178
  getQuotaSnapshots,
156
179
  summarizeSessionUsageForDisplay,
157
180
  scheduleParentRefreshIfSafe,
181
+ isSessionActive,
158
182
  restoreConcurrency: RESTORE_TITLE_CONCURRENCY,
159
183
  });
160
184
  const titleRefresh = createTitleRefreshScheduler({
161
185
  apply: async (sessionID) => {
186
+ if (!isSessionActive(sessionID))
187
+ return;
162
188
  await titleApplicator.applyTitle(sessionID);
163
189
  },
164
190
  onError: swallow('titleRefresh'),
165
191
  });
166
192
  scheduleTitleRefresh = titleRefresh.schedule;
167
- const restoreAllVisibleTitles = titleApplicator.restoreAllVisibleTitles;
168
- const refreshAllTouchedTitles = titleApplicator.refreshAllTouchedTitles;
169
- const refreshAllVisibleTitles = titleApplicator.refreshAllVisibleTitles;
170
- let startupTitleWork = Promise.resolve();
171
- const runStartupRestore = async (attempt = 0) => {
172
- const result = await restoreAllVisibleTitles({
173
- abortIfEnabled: config.sidebar.enabled,
174
- });
175
- if (result.restored === result.attempted)
176
- return;
177
- debug(`startup restore incomplete: restored ${result.restored}/${result.attempted} touched titles while display mode remains OFF`);
178
- if (state.titleEnabled || config.sidebar.enabled === false)
179
- return;
180
- if (attempt >= 2)
181
- return;
182
- await new Promise((resolve) => setTimeout(resolve, 1_000));
183
- await runStartupRestore(attempt + 1);
184
- };
185
- if (!state.titleEnabled || !config.sidebar.enabled) {
186
- startupTitleWork = runStartupRestore().catch(swallow('startup:restoreAllVisibleTitles'));
187
- }
188
- else {
189
- startupTitleWork = Promise.allSettled([
190
- refreshAllVisibleTitles().catch(swallow('startup:refreshAllVisibleTitles')),
191
- refreshAllTouchedTitles().catch(swallow('startup:refreshAllTouchedTitles')),
192
- ]).then(() => undefined);
193
- }
193
+ const startupTitleWork = Promise.resolve();
194
194
  const shutdown = async () => {
195
195
  await Promise.race([
196
196
  startupTitleWork,
@@ -318,7 +318,7 @@ export async function QuotaSidebarPlugin(input) {
318
318
  sessionID: session.id,
319
319
  incomingTitle: session.title,
320
320
  sessionState,
321
- scheduleRefresh: titleRefresh.schedule,
321
+ scheduleRefresh: scheduleActiveTitleRefresh,
322
322
  });
323
323
  },
324
324
  onSessionDeleted: async (session) => {
@@ -328,6 +328,7 @@ export async function QuotaSidebarPlugin(input) {
328
328
  usageService.forgetSession(session.id);
329
329
  titleApplicator.forgetSession(session.id);
330
330
  titleRefresh.cancel(session.id);
331
+ clearSessionActivity(session.id);
331
332
  const dateKey = state.sessionDateMap[session.id] ||
332
333
  dateKeyFromTimestamp(session.time.created);
333
334
  state.deletedSessionDateMap[session.id] = dateKey;
@@ -341,23 +342,34 @@ export async function QuotaSidebarPlugin(input) {
341
342
  scheduleSave();
342
343
  }
343
344
  if (config.sidebar.includeChildren && session.parentID) {
344
- titleRefresh.schedule(session.parentID, 0);
345
+ scheduleActiveTitleRefresh(session.parentID, 0);
345
346
  }
346
347
  },
347
348
  onTuiActivity: async () => {
348
349
  return;
349
350
  },
350
351
  onTuiSessionSelect: async (sessionID) => {
351
- titleRefresh.schedule(sessionID, 0);
352
+ scheduleActiveTitleRefresh(sessionID, 0);
352
353
  },
353
354
  onMessageRemoved: async (info) => {
354
355
  usageService.markForceRescan(info.sessionID);
355
- titleRefresh.schedule(info.sessionID, 0);
356
+ scheduleActiveTitleRefresh(info.sessionID, 0);
356
357
  scheduleParentRefreshIfSafe(info.sessionID, state.sessions[info.sessionID]?.parentID);
357
358
  },
358
- onAssistantMessageCompleted: async (message) => {
359
+ onAssistantMessageUpdated: async (message) => {
360
+ const now = Date.now();
361
+ const completed = message.time.completed;
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) {
368
+ return;
369
+ }
370
+ markSessionActive(message.sessionID, now);
359
371
  usageService.markSessionDirty(message.sessionID);
360
- titleRefresh.schedule(message.sessionID);
372
+ scheduleActiveTitleRefresh(message.sessionID);
361
373
  void maybeShowExpiryToast(message.sessionID);
362
374
  },
363
375
  });
@@ -378,14 +390,13 @@ export async function QuotaSidebarPlugin(input) {
378
390
  scheduleSave,
379
391
  flushSave,
380
392
  waitForStartupTitleWork: () => startupTitleWork,
381
- refreshSessionTitle: (sessionID, delay) => titleRefresh.schedule(sessionID, delay ?? 250),
393
+ markSessionActive,
394
+ refreshSessionTitle: (sessionID, delay) => scheduleActiveTitleRefresh(sessionID, delay ?? 250),
382
395
  cancelAllTitleRefreshes: () => titleRefresh.cancelAll(),
383
396
  flushScheduledTitleRefreshes: () => titleRefresh.flushScheduled(),
384
397
  waitForTitleRefreshIdle: () => titleRefresh.waitForIdle(),
385
398
  waitForTitleRefreshQuiescence: () => titleRefresh.waitForQuiescence(),
386
- restoreAllVisibleTitles,
387
- refreshAllTouchedTitles,
388
- refreshAllVisibleTitles,
399
+ restoreSessionTitle: (sessionID) => titleApplicator.restoreSessionTitle(sessionID),
389
400
  showToast,
390
401
  summarizeForTool,
391
402
  getQuotaSnapshots,
@@ -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,41 +63,35 @@ export function createQuotaSidebarTools(deps) {
63
63
  deps.setTitleEnabled(true);
64
64
  deps.scheduleSave();
65
65
  await deps.flushSave();
66
- const visible = await deps.refreshAllVisibleTitles();
67
- const touched = await deps.refreshAllTouchedTitles();
66
+ deps.markSessionActive?.(context.sessionID);
68
67
  deps.refreshSessionTitle(context.sessionID, 0);
69
68
  if (startupTimedOut) {
70
69
  void deps.waitForStartupTitleWork().then(() => {
71
70
  if (!deps.getTitleEnabled())
72
71
  return;
73
- void deps.refreshAllVisibleTitles();
74
- void deps.refreshAllTouchedTitles();
72
+ deps.markSessionActive?.(context.sessionID);
75
73
  deps.refreshSessionTitle(context.sessionID, 0);
76
74
  });
77
75
  }
78
76
  await deps.showToast('toggle', 'Sidebar usage display: ON');
79
- if (visible.listFailed ||
80
- visible.refreshed < visible.attempted ||
81
- touched.refreshed < touched.attempted) {
82
- return 'Sidebar usage display is now ON. Visible-session refresh failed, so only touched/current session titles are guaranteed to refresh immediately.';
83
- }
84
- return 'Sidebar usage display is now ON. Visible session titles are refreshing to show token usage and quota.';
77
+ return 'Sidebar usage display is now ON. Only assistant-active sessions will refresh shared titles.';
85
78
  }
86
79
  deps.setTitleEnabled(false);
87
80
  deps.scheduleSave();
88
81
  await deps.flushSave();
89
82
  deps.cancelAllTitleRefreshes();
90
83
  await deps.waitForTitleRefreshQuiescence();
91
- const restore = await deps.restoreAllVisibleTitles();
92
- if (restore.restored === restore.attempted) {
84
+ const restoredCurrent = await (deps.restoreSessionTitle
85
+ ? deps.restoreSessionTitle(context.sessionID)
86
+ : Promise.resolve(false));
87
+ if (restoredCurrent) {
93
88
  await deps.showToast('toggle', 'Sidebar usage display: OFF');
94
- 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.';
95
90
  }
96
91
  deps.setTitleEnabled(true);
97
92
  deps.scheduleSave();
98
93
  await deps.flushSave();
99
- await deps.refreshAllVisibleTitles();
100
- await deps.refreshAllTouchedTitles();
94
+ deps.markSessionActive?.(context.sessionID);
101
95
  deps.refreshSessionTitle(context.sessionID, 0);
102
96
  await deps.showToast('toggle', 'Sidebar usage display: OFF failed');
103
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.5",
3
+ "version": "3.0.9",
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",