@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 +1 -1
- package/dist/events.js +3 -5
- package/dist/index.js +49 -38
- package/dist/title_apply.d.ts +1 -0
- package/dist/title_apply.js +4 -0
- package/dist/tools.d.ts +2 -15
- package/dist/tools.js +9 -15
- package/package.json +1 -1
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
|
-
|
|
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' ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
345
|
+
scheduleActiveTitleRefresh(session.parentID, 0);
|
|
345
346
|
}
|
|
346
347
|
},
|
|
347
348
|
onTuiActivity: async () => {
|
|
348
349
|
return;
|
|
349
350
|
},
|
|
350
351
|
onTuiSessionSelect: async (sessionID) => {
|
|
351
|
-
|
|
352
|
+
scheduleActiveTitleRefresh(sessionID, 0);
|
|
352
353
|
},
|
|
353
354
|
onMessageRemoved: async (info) => {
|
|
354
355
|
usageService.markForceRescan(info.sessionID);
|
|
355
|
-
|
|
356
|
+
scheduleActiveTitleRefresh(info.sessionID, 0);
|
|
356
357
|
scheduleParentRefreshIfSafe(info.sessionID, state.sessions[info.sessionID]?.parentID);
|
|
357
358
|
},
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
refreshAllTouchedTitles,
|
|
388
|
-
refreshAllVisibleTitles,
|
|
399
|
+
restoreSessionTitle: (sessionID) => titleApplicator.restoreSessionTitle(sessionID),
|
|
389
400
|
showToast,
|
|
390
401
|
summarizeForTool,
|
|
391
402
|
getQuotaSnapshots,
|
package/dist/title_apply.d.ts
CHANGED
|
@@ -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>;
|
package/dist/title_apply.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
92
|
-
|
|
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.
|
|
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
|
-
|
|
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