@leo000001/opencode-quota-sidebar 2.0.0 → 2.0.2
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/README.md +26 -19
- package/dist/cost.js +2 -1
- package/dist/format.js +155 -52
- package/dist/index.js +77 -4
- package/dist/persistence.js +15 -1
- package/dist/quota.js +3 -5
- package/dist/quota_service.js +194 -29
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +74 -9
- package/dist/storage_chunks.js +20 -9
- package/dist/storage_parse.js +4 -1
- package/dist/title.js +37 -15
- package/dist/title_apply.d.ts +21 -3
- package/dist/title_apply.js +109 -23
- package/dist/title_refresh.d.ts +4 -0
- package/dist/title_refresh.js +35 -1
- package/dist/tools.d.ts +22 -1
- package/dist/tools.js +60 -14
- package/dist/types.d.ts +27 -1
- package/dist/usage.d.ts +14 -6
- package/dist/usage.js +78 -13
- package/dist/usage_service.js +159 -55
- package/package.json +1 -1
package/dist/title_apply.d.ts
CHANGED
|
@@ -17,14 +17,32 @@ export declare function createTitleApplicator(deps: {
|
|
|
17
17
|
scheduleParentRefreshIfSafe: (sessionID: string, parentID?: string) => void;
|
|
18
18
|
restoreConcurrency: number;
|
|
19
19
|
}): {
|
|
20
|
-
applyTitle: (sessionID: string) => Promise<
|
|
20
|
+
applyTitle: (sessionID: string) => Promise<boolean>;
|
|
21
21
|
handleSessionUpdatedTitle: (args: {
|
|
22
22
|
sessionID: string;
|
|
23
23
|
incomingTitle: string;
|
|
24
24
|
sessionState: SessionState;
|
|
25
25
|
scheduleRefresh: (sessionID: string, delay?: number) => void;
|
|
26
26
|
}) => Promise<void>;
|
|
27
|
-
restoreSessionTitle: (sessionID: string
|
|
28
|
-
|
|
27
|
+
restoreSessionTitle: (sessionID: string, options?: {
|
|
28
|
+
abortIfEnabled?: boolean;
|
|
29
|
+
}) => Promise<boolean>;
|
|
30
|
+
restoreAllVisibleTitles: (options?: {
|
|
31
|
+
abortIfEnabled?: boolean;
|
|
32
|
+
}) => Promise<{
|
|
33
|
+
attempted: number;
|
|
34
|
+
restored: number;
|
|
35
|
+
listFailed: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
refreshAllTouchedTitles: () => Promise<{
|
|
38
|
+
attempted: number;
|
|
39
|
+
refreshed: number;
|
|
40
|
+
listFailed: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
refreshAllVisibleTitles: () => Promise<{
|
|
43
|
+
attempted: number;
|
|
44
|
+
refreshed: number;
|
|
45
|
+
listFailed: boolean;
|
|
46
|
+
}>;
|
|
29
47
|
forgetSession: (sessionID: string) => void;
|
|
30
48
|
};
|
package/dist/title_apply.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated,
|
|
1
|
+
import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, } from './title.js';
|
|
2
2
|
import { swallow, debug, mapConcurrent } from './helpers.js';
|
|
3
3
|
export function createTitleApplicator(deps) {
|
|
4
4
|
const pendingAppliedTitle = new Map();
|
|
5
|
+
const recentRestore = new Map();
|
|
5
6
|
const forgetSession = (sessionID) => {
|
|
6
7
|
pendingAppliedTitle.delete(sessionID);
|
|
8
|
+
recentRestore.delete(sessionID);
|
|
7
9
|
};
|
|
8
10
|
const applyTitle = async (sessionID) => {
|
|
9
11
|
if (!deps.config.sidebar.enabled || !deps.state.titleEnabled)
|
|
10
|
-
return;
|
|
12
|
+
return false;
|
|
11
13
|
let stateMutated = false;
|
|
12
14
|
const session = await deps.client.session
|
|
13
15
|
.get({
|
|
@@ -17,7 +19,14 @@ export function createTitleApplicator(deps) {
|
|
|
17
19
|
})
|
|
18
20
|
.catch(swallow('applyTitle:getSession'));
|
|
19
21
|
if (!session)
|
|
20
|
-
return;
|
|
22
|
+
return false;
|
|
23
|
+
if (!session.data ||
|
|
24
|
+
typeof session.data.title !== 'string' ||
|
|
25
|
+
!session.data.time ||
|
|
26
|
+
typeof session.data.time.created !== 'number') {
|
|
27
|
+
debug(`applyTitle skipped malformed session payload for ${sessionID}`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
21
30
|
const sessionState = deps.ensureSessionState(sessionID, session.data.title, session.data.time.created, session.data.parentID ?? null);
|
|
22
31
|
// Detect whether the current title is our own decorated form.
|
|
23
32
|
const currentTitle = session.data.title;
|
|
@@ -44,7 +53,7 @@ export function createTitleApplicator(deps) {
|
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
else {
|
|
47
|
-
const nextBase =
|
|
56
|
+
const nextBase = canonicalizeTitle(currentTitle) || 'Session';
|
|
48
57
|
if (sessionState.baseTitle !== nextBase) {
|
|
49
58
|
sessionState.baseTitle = nextBase;
|
|
50
59
|
stateMutated = true;
|
|
@@ -61,6 +70,8 @@ export function createTitleApplicator(deps) {
|
|
|
61
70
|
? await deps.getQuotaSnapshots(quotaProviders)
|
|
62
71
|
: [];
|
|
63
72
|
const nextTitle = deps.renderSidebarTitle(sessionState.baseTitle, usage, quotas, deps.config);
|
|
73
|
+
if (!deps.config.sidebar.enabled || !deps.state.titleEnabled)
|
|
74
|
+
return false;
|
|
64
75
|
if (canonicalizeTitleForCompare(nextTitle) ===
|
|
65
76
|
canonicalizeTitleForCompare(session.data.title)) {
|
|
66
77
|
if (looksDecorated(session.data.title)) {
|
|
@@ -74,7 +85,7 @@ export function createTitleApplicator(deps) {
|
|
|
74
85
|
}
|
|
75
86
|
deps.scheduleSave();
|
|
76
87
|
deps.scheduleParentRefreshIfSafe(sessionID, sessionState.parentID);
|
|
77
|
-
return;
|
|
88
|
+
return true;
|
|
78
89
|
}
|
|
79
90
|
// Mark pending title to ignore the immediate echo `session.updated` event.
|
|
80
91
|
// H3 fix: use longer TTL (15s) and add decoration detection as backup.
|
|
@@ -98,11 +109,12 @@ export function createTitleApplicator(deps) {
|
|
|
98
109
|
sessionState.lastAppliedTitle = previousApplied;
|
|
99
110
|
deps.scheduleSave();
|
|
100
111
|
deps.scheduleParentRefreshIfSafe(sessionID, sessionState.parentID);
|
|
101
|
-
return;
|
|
112
|
+
return false;
|
|
102
113
|
}
|
|
103
114
|
pendingAppliedTitle.delete(sessionID);
|
|
104
115
|
deps.scheduleSave();
|
|
105
116
|
deps.scheduleParentRefreshIfSafe(sessionID, sessionState.parentID);
|
|
117
|
+
return true;
|
|
106
118
|
};
|
|
107
119
|
const handleSessionUpdatedTitle = async (args) => {
|
|
108
120
|
const pending = pendingAppliedTitle.get(args.sessionID);
|
|
@@ -129,17 +141,39 @@ export function createTitleApplicator(deps) {
|
|
|
129
141
|
canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle || '')) {
|
|
130
142
|
return;
|
|
131
143
|
}
|
|
132
|
-
if (looksDecorated(args.incomingTitle)) {
|
|
133
|
-
|
|
144
|
+
if (looksDecorated(args.incomingTitle) && args.sessionState.lastAppliedTitle) {
|
|
145
|
+
if (canonicalizeTitleForCompare(args.incomingTitle) ===
|
|
146
|
+
canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle)) {
|
|
147
|
+
debug(`ignoring late decorated echo for session ${args.sessionID}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (looksDecorated(args.incomingTitle) && !args.sessionState.lastAppliedTitle) {
|
|
152
|
+
debug(`ignoring untracked decorated title for session ${args.sessionID}`);
|
|
134
153
|
return;
|
|
135
154
|
}
|
|
136
|
-
|
|
155
|
+
const restored = recentRestore.get(args.sessionID);
|
|
156
|
+
if (restored) {
|
|
157
|
+
if (restored.expiresAt <= Date.now()) {
|
|
158
|
+
recentRestore.delete(args.sessionID);
|
|
159
|
+
}
|
|
160
|
+
else if (looksDecorated(args.incomingTitle) &&
|
|
161
|
+
(!restored.decoratedTitle ||
|
|
162
|
+
canonicalizeTitleForCompare(args.incomingTitle) ===
|
|
163
|
+
canonicalizeTitleForCompare(restored.decoratedTitle))) {
|
|
164
|
+
debug(`ignoring decorated echo after restore for session ${args.sessionID}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
args.sessionState.baseTitle = canonicalizeTitle(args.incomingTitle) || 'Session';
|
|
137
169
|
args.sessionState.lastAppliedTitle = undefined;
|
|
138
170
|
deps.markDirty(deps.state.sessionDateMap[args.sessionID]);
|
|
139
171
|
deps.scheduleSave();
|
|
140
172
|
args.scheduleRefresh(args.sessionID);
|
|
141
173
|
};
|
|
142
|
-
const restoreSessionTitle = async (sessionID) => {
|
|
174
|
+
const restoreSessionTitle = async (sessionID, options) => {
|
|
175
|
+
if (options?.abortIfEnabled && deps.state.titleEnabled)
|
|
176
|
+
return false;
|
|
143
177
|
const session = await deps.client.session
|
|
144
178
|
.get({
|
|
145
179
|
path: { id: sessionID },
|
|
@@ -148,12 +182,27 @@ export function createTitleApplicator(deps) {
|
|
|
148
182
|
})
|
|
149
183
|
.catch(swallow('restoreSessionTitle:get'));
|
|
150
184
|
if (!session)
|
|
151
|
-
return;
|
|
185
|
+
return false;
|
|
186
|
+
if (!session.data ||
|
|
187
|
+
typeof session.data.title !== 'string' ||
|
|
188
|
+
!session.data.time ||
|
|
189
|
+
typeof session.data.time.created !== 'number') {
|
|
190
|
+
debug(`restoreSessionTitle skipped malformed session payload for ${sessionID}`);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
152
193
|
const sessionState = deps.ensureSessionState(sessionID, session.data.title, session.data.time.created, session.data.parentID ?? null);
|
|
153
|
-
const baseTitle =
|
|
154
|
-
if (session.data.title === baseTitle)
|
|
155
|
-
|
|
156
|
-
|
|
194
|
+
const baseTitle = canonicalizeTitle(sessionState.baseTitle) || 'Session';
|
|
195
|
+
if (session.data.title === baseTitle) {
|
|
196
|
+
if (sessionState.lastAppliedTitle !== undefined) {
|
|
197
|
+
sessionState.lastAppliedTitle = undefined;
|
|
198
|
+
deps.markDirty(deps.state.sessionDateMap[sessionID]);
|
|
199
|
+
deps.scheduleSave();
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (options?.abortIfEnabled && deps.state.titleEnabled)
|
|
204
|
+
return false;
|
|
205
|
+
const updated = await deps.client.session
|
|
157
206
|
.update({
|
|
158
207
|
path: { id: sessionID },
|
|
159
208
|
query: { directory: deps.directory },
|
|
@@ -161,29 +210,66 @@ export function createTitleApplicator(deps) {
|
|
|
161
210
|
throwOnError: true,
|
|
162
211
|
})
|
|
163
212
|
.catch(swallow('restoreSessionTitle:update'));
|
|
213
|
+
if (!updated)
|
|
214
|
+
return false;
|
|
215
|
+
pendingAppliedTitle.delete(sessionID);
|
|
216
|
+
recentRestore.set(sessionID, {
|
|
217
|
+
baseTitle,
|
|
218
|
+
decoratedTitle: sessionState.lastAppliedTitle,
|
|
219
|
+
expiresAt: Date.now() + 15_000,
|
|
220
|
+
});
|
|
164
221
|
sessionState.lastAppliedTitle = undefined;
|
|
165
222
|
deps.markDirty(deps.state.sessionDateMap[sessionID]);
|
|
166
223
|
deps.scheduleSave();
|
|
224
|
+
return true;
|
|
225
|
+
};
|
|
226
|
+
const restoreAllVisibleTitles = async (options) => {
|
|
227
|
+
const touched = Object.entries(deps.state.sessions)
|
|
228
|
+
.filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
|
|
229
|
+
.map(([sessionID]) => sessionID);
|
|
230
|
+
const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => restoreSessionTitle(sessionID, options));
|
|
231
|
+
return {
|
|
232
|
+
attempted: touched.length,
|
|
233
|
+
restored: results.filter(Boolean).length,
|
|
234
|
+
listFailed: false,
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
const refreshAllTouchedTitles = async () => {
|
|
238
|
+
const touched = Object.entries(deps.state.sessions)
|
|
239
|
+
.filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
|
|
240
|
+
.map(([sessionID]) => sessionID);
|
|
241
|
+
const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => applyTitle(sessionID));
|
|
242
|
+
return {
|
|
243
|
+
attempted: touched.length,
|
|
244
|
+
refreshed: results.filter(Boolean).length,
|
|
245
|
+
listFailed: false,
|
|
246
|
+
};
|
|
167
247
|
};
|
|
168
|
-
const
|
|
248
|
+
const refreshAllVisibleTitles = async () => {
|
|
169
249
|
const list = await deps.client.session
|
|
170
250
|
.list({
|
|
171
251
|
query: { directory: deps.directory },
|
|
172
252
|
throwOnError: true,
|
|
173
253
|
})
|
|
174
|
-
.catch(swallow('
|
|
175
|
-
if (!list?.data)
|
|
176
|
-
return;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
254
|
+
.catch(swallow('refreshAllVisibleTitles:list'));
|
|
255
|
+
if (!list?.data || !Array.isArray(list.data)) {
|
|
256
|
+
return { attempted: 0, refreshed: 0, listFailed: true };
|
|
257
|
+
}
|
|
258
|
+
const sessions = list.data.filter((session) => Boolean(session && typeof session.id === 'string'));
|
|
259
|
+
const results = await mapConcurrent(sessions, deps.restoreConcurrency, async (session) => applyTitle(session.id));
|
|
260
|
+
return {
|
|
261
|
+
attempted: sessions.length,
|
|
262
|
+
refreshed: results.filter(Boolean).length,
|
|
263
|
+
listFailed: false,
|
|
264
|
+
};
|
|
181
265
|
};
|
|
182
266
|
return {
|
|
183
267
|
applyTitle,
|
|
184
268
|
handleSessionUpdatedTitle,
|
|
185
269
|
restoreSessionTitle,
|
|
186
270
|
restoreAllVisibleTitles,
|
|
271
|
+
refreshAllTouchedTitles,
|
|
272
|
+
refreshAllVisibleTitles,
|
|
187
273
|
forgetSession,
|
|
188
274
|
};
|
|
189
275
|
}
|
package/dist/title_refresh.d.ts
CHANGED
|
@@ -5,5 +5,9 @@ export declare function createTitleRefreshScheduler(options: {
|
|
|
5
5
|
schedule: (sessionID: string, delay?: number) => void;
|
|
6
6
|
apply: (sessionID: string) => Promise<void>;
|
|
7
7
|
cancel: (sessionID: string) => void;
|
|
8
|
+
cancelAll: () => void;
|
|
9
|
+
flushScheduled: () => Promise<void>;
|
|
10
|
+
waitForIdle: (timeoutMs?: number) => Promise<void>;
|
|
11
|
+
waitForQuiescence: (budgetMs?: number) => Promise<void>;
|
|
8
12
|
dispose: () => void;
|
|
9
13
|
};
|
package/dist/title_refresh.js
CHANGED
|
@@ -31,16 +31,50 @@ export function createTitleRefreshScheduler(options) {
|
|
|
31
31
|
clearTimeout(timer);
|
|
32
32
|
refreshTimer.delete(sessionID);
|
|
33
33
|
};
|
|
34
|
-
const
|
|
34
|
+
const cancelAll = () => {
|
|
35
35
|
for (const timer of refreshTimer.values())
|
|
36
36
|
clearTimeout(timer);
|
|
37
37
|
refreshTimer.clear();
|
|
38
|
+
};
|
|
39
|
+
const flushScheduled = async () => {
|
|
40
|
+
const pending = Array.from(refreshTimer.keys());
|
|
41
|
+
cancelAll();
|
|
42
|
+
await Promise.allSettled(pending.map((sessionID) => applyLocked(sessionID)));
|
|
43
|
+
};
|
|
44
|
+
const waitForIdle = async (timeoutMs) => {
|
|
45
|
+
const inflight = Array.from(applyLocks.values());
|
|
46
|
+
if (inflight.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
if (timeoutMs === undefined) {
|
|
49
|
+
await Promise.allSettled(inflight);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await Promise.race([
|
|
53
|
+
Promise.allSettled(inflight),
|
|
54
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
55
|
+
]);
|
|
56
|
+
};
|
|
57
|
+
const waitForQuiescence = async (budgetMs = 10_000) => {
|
|
58
|
+
const deadline = Date.now() + budgetMs;
|
|
59
|
+
while (Date.now() < deadline) {
|
|
60
|
+
await flushScheduled();
|
|
61
|
+
await waitForIdle(Math.max(0, deadline - Date.now()));
|
|
62
|
+
if (refreshTimer.size === 0 && applyLocks.size === 0)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const dispose = () => {
|
|
67
|
+
cancelAll();
|
|
38
68
|
applyLocks.clear();
|
|
39
69
|
};
|
|
40
70
|
return {
|
|
41
71
|
schedule,
|
|
42
72
|
apply: applyLocked,
|
|
43
73
|
cancel,
|
|
74
|
+
cancelAll,
|
|
75
|
+
flushScheduled,
|
|
76
|
+
waitForIdle,
|
|
77
|
+
waitForQuiescence,
|
|
44
78
|
dispose,
|
|
45
79
|
};
|
|
46
80
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -4,8 +4,28 @@ export declare function createQuotaSidebarTools(deps: {
|
|
|
4
4
|
getTitleEnabled: () => boolean;
|
|
5
5
|
setTitleEnabled: (enabled: boolean) => void;
|
|
6
6
|
scheduleSave: () => void;
|
|
7
|
+
flushSave: () => Promise<void>;
|
|
8
|
+
waitForStartupTitleWork: () => Promise<void>;
|
|
7
9
|
refreshSessionTitle: (sessionID: string, delay?: number) => void;
|
|
8
|
-
|
|
10
|
+
cancelAllTitleRefreshes: () => void;
|
|
11
|
+
flushScheduledTitleRefreshes: () => Promise<void>;
|
|
12
|
+
waitForTitleRefreshIdle: () => Promise<void>;
|
|
13
|
+
waitForTitleRefreshQuiescence: () => Promise<void>;
|
|
14
|
+
restoreAllVisibleTitles: () => Promise<{
|
|
15
|
+
attempted: number;
|
|
16
|
+
restored: number;
|
|
17
|
+
listFailed: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
refreshAllTouchedTitles: () => Promise<{
|
|
20
|
+
attempted: number;
|
|
21
|
+
refreshed: number;
|
|
22
|
+
listFailed: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
refreshAllVisibleTitles: () => Promise<{
|
|
25
|
+
attempted: number;
|
|
26
|
+
refreshed: number;
|
|
27
|
+
listFailed: boolean;
|
|
28
|
+
}>;
|
|
9
29
|
showToast: (period: 'session' | 'day' | 'week' | 'month' | 'toggle', message: string) => Promise<void>;
|
|
10
30
|
summarizeForTool: (period: 'session' | 'day' | 'week' | 'month', sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
|
|
11
31
|
getQuotaSnapshots: (providerIDs: string[], options?: {
|
|
@@ -24,6 +44,7 @@ export declare function createQuotaSidebarTools(deps: {
|
|
|
24
44
|
width: number;
|
|
25
45
|
includeChildren: boolean;
|
|
26
46
|
};
|
|
47
|
+
sidebarEnabled: boolean;
|
|
27
48
|
};
|
|
28
49
|
}): {
|
|
29
50
|
quota_summary: {
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
2
|
const z = tool.schema;
|
|
3
3
|
export function createQuotaSidebarTools(deps) {
|
|
4
|
+
let toggleLock = Promise.resolve();
|
|
5
|
+
const waitForStartupTitleWork = async () => {
|
|
6
|
+
const timedOut = await Promise.race([
|
|
7
|
+
deps.waitForStartupTitleWork(),
|
|
8
|
+
new Promise((resolve) => setTimeout(() => resolve('timeout'), 3_000)),
|
|
9
|
+
]);
|
|
10
|
+
return timedOut === 'timeout';
|
|
11
|
+
};
|
|
4
12
|
return {
|
|
5
13
|
quota_summary: tool({
|
|
6
14
|
description: 'Show usage and quota summary for session/day/week/month.',
|
|
@@ -18,7 +26,6 @@ export function createQuotaSidebarTools(deps) {
|
|
|
18
26
|
? (args.includeChildren ?? deps.config.sidebar.includeChildren)
|
|
19
27
|
: false;
|
|
20
28
|
const usage = await deps.summarizeForTool(period, context.sessionID, includeChildren);
|
|
21
|
-
deps.scheduleSave();
|
|
22
29
|
// For quota_summary, always show all subscription quota balances,
|
|
23
30
|
// regardless of which providers were used in the session.
|
|
24
31
|
const quotas = await deps.getQuotaSnapshots([], { allowDefault: true });
|
|
@@ -43,20 +50,59 @@ export function createQuotaSidebarTools(deps) {
|
|
|
43
50
|
.describe('Explicit on/off. Omit to toggle current state.'),
|
|
44
51
|
},
|
|
45
52
|
execute: async (args, context) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const run = async () => {
|
|
54
|
+
const current = deps.getTitleEnabled();
|
|
55
|
+
const next = args.enabled !== undefined ? args.enabled : !current;
|
|
56
|
+
if (next) {
|
|
57
|
+
if (!deps.config.sidebarEnabled) {
|
|
58
|
+
return 'Sidebar usage display cannot be enabled because `sidebar.enabled=false` in config. Re-enable the sidebar feature first.';
|
|
59
|
+
}
|
|
60
|
+
const startupTimedOut = await waitForStartupTitleWork();
|
|
61
|
+
deps.setTitleEnabled(true);
|
|
62
|
+
deps.scheduleSave();
|
|
63
|
+
await deps.flushSave();
|
|
64
|
+
const visible = await deps.refreshAllVisibleTitles();
|
|
65
|
+
const touched = await deps.refreshAllTouchedTitles();
|
|
66
|
+
deps.refreshSessionTitle(context.sessionID, 0);
|
|
67
|
+
if (startupTimedOut) {
|
|
68
|
+
void deps.waitForStartupTitleWork().then(() => {
|
|
69
|
+
if (!deps.getTitleEnabled())
|
|
70
|
+
return;
|
|
71
|
+
void deps.refreshAllVisibleTitles();
|
|
72
|
+
void deps.refreshAllTouchedTitles();
|
|
73
|
+
deps.refreshSessionTitle(context.sessionID, 0);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
await deps.showToast('toggle', 'Sidebar usage display: ON');
|
|
77
|
+
if (visible.listFailed ||
|
|
78
|
+
visible.refreshed < visible.attempted ||
|
|
79
|
+
touched.refreshed < touched.attempted) {
|
|
80
|
+
return 'Sidebar usage display is now ON. Visible-session refresh failed, so only touched/current session titles are guaranteed to refresh immediately.';
|
|
81
|
+
}
|
|
82
|
+
return 'Sidebar usage display is now ON. Visible session titles are refreshing to show token usage and quota.';
|
|
83
|
+
}
|
|
84
|
+
deps.setTitleEnabled(false);
|
|
85
|
+
deps.scheduleSave();
|
|
86
|
+
await deps.flushSave();
|
|
87
|
+
deps.cancelAllTitleRefreshes();
|
|
88
|
+
await deps.waitForTitleRefreshQuiescence();
|
|
89
|
+
const restore = await deps.restoreAllVisibleTitles();
|
|
90
|
+
if (restore.restored === restore.attempted) {
|
|
91
|
+
await deps.showToast('toggle', 'Sidebar usage display: OFF');
|
|
92
|
+
return 'Sidebar usage display is now OFF. Touched session titles were restored to base titles.';
|
|
93
|
+
}
|
|
94
|
+
deps.setTitleEnabled(true);
|
|
95
|
+
deps.scheduleSave();
|
|
96
|
+
await deps.flushSave();
|
|
97
|
+
await deps.refreshAllVisibleTitles();
|
|
98
|
+
await deps.refreshAllTouchedTitles();
|
|
52
99
|
deps.refreshSessionTitle(context.sessionID, 0);
|
|
53
|
-
await deps.showToast('toggle', 'Sidebar usage display:
|
|
54
|
-
return 'Sidebar usage display
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return 'Sidebar usage display is now OFF. Session titles restored to original.';
|
|
100
|
+
await deps.showToast('toggle', 'Sidebar usage display: OFF failed');
|
|
101
|
+
return 'Sidebar usage display remains ON because some touched session titles could not be restored. Try again after the session service recovers.';
|
|
102
|
+
};
|
|
103
|
+
const pending = toggleLock.then(run, run);
|
|
104
|
+
toggleLock = pending.then(() => undefined, () => undefined);
|
|
105
|
+
return pending;
|
|
60
106
|
},
|
|
61
107
|
}),
|
|
62
108
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,20 @@ export type CacheUsageBuckets = {
|
|
|
47
47
|
readOnly: CacheUsageBucket;
|
|
48
48
|
readWrite: CacheUsageBucket;
|
|
49
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Derived cache coverage metrics.
|
|
52
|
+
*
|
|
53
|
+
* - `cacheCoverage`: fraction of prompt surface covered by read-write cache
|
|
54
|
+
* (`(cacheRead + cacheWrite) / (input + cacheRead + cacheWrite)`).
|
|
55
|
+
* Only defined when the read-write bucket has traffic.
|
|
56
|
+
* - `cacheReadCoverage`: fraction of prompt surface served from read-only cache
|
|
57
|
+
* (`cacheRead / (input + cacheRead)`).
|
|
58
|
+
* Only defined when the read-only bucket has traffic.
|
|
59
|
+
*/
|
|
60
|
+
export type CacheCoverageMetrics = {
|
|
61
|
+
cacheCoverage: number | undefined;
|
|
62
|
+
cacheReadCoverage: number | undefined;
|
|
63
|
+
};
|
|
50
64
|
export type CachedProviderUsage = {
|
|
51
65
|
input: number;
|
|
52
66
|
output: number;
|
|
@@ -58,6 +72,8 @@ export type CachedProviderUsage = {
|
|
|
58
72
|
/** Equivalent API billing cost (USD) computed from model pricing. */
|
|
59
73
|
apiCost: number;
|
|
60
74
|
assistantMessages: number;
|
|
75
|
+
/** Provider-level cache coverage buckets grouped by model cache behavior. */
|
|
76
|
+
cacheBuckets?: CacheUsageBuckets;
|
|
61
77
|
};
|
|
62
78
|
export type CachedSessionUsage = {
|
|
63
79
|
/** Billing aggregation cache version for cost/apiCost refresh migrations. */
|
|
@@ -72,7 +88,13 @@ export type CachedSessionUsage = {
|
|
|
72
88
|
/** Equivalent API billing cost (USD) computed from model pricing. */
|
|
73
89
|
apiCost: number;
|
|
74
90
|
assistantMessages: number;
|
|
75
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Cache coverage buckets grouped by model cache behavior.
|
|
93
|
+
*
|
|
94
|
+
* `undefined` when no cache-capable models were used or data predates
|
|
95
|
+
* billingVersion 3. The fallback in `resolvedCacheUsageBuckets()` derives
|
|
96
|
+
* approximate buckets from top-level `cacheRead`/`cacheWrite` when missing.
|
|
97
|
+
*/
|
|
76
98
|
cacheBuckets?: CacheUsageBuckets;
|
|
77
99
|
providers: Record<string, CachedProviderUsage>;
|
|
78
100
|
};
|
|
@@ -90,6 +112,8 @@ export type SessionState = SessionTitleState & {
|
|
|
90
112
|
/** Parent session ID for subagent child sessions. */
|
|
91
113
|
parentID?: string;
|
|
92
114
|
usage?: CachedSessionUsage;
|
|
115
|
+
/** Persisted dirtiness flag so descendant aggregation survives restart. */
|
|
116
|
+
dirty?: boolean;
|
|
93
117
|
/** Incremental aggregation cursor (P1). */
|
|
94
118
|
cursor?: IncrementalCursor;
|
|
95
119
|
};
|
|
@@ -104,6 +128,8 @@ export type QuotaSidebarState = {
|
|
|
104
128
|
titleEnabled: boolean;
|
|
105
129
|
sessionDateMap: Record<string, string>;
|
|
106
130
|
sessions: Record<string, SessionState>;
|
|
131
|
+
/** Tombstones for sessions deleted from memory but not yet purged from day chunks. */
|
|
132
|
+
deletedSessionDateMap: Record<string, string>;
|
|
107
133
|
quotaCache: Record<string, QuotaSnapshot>;
|
|
108
134
|
};
|
|
109
135
|
export type QuotaSidebarConfig = {
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AssistantMessage, Message } from '@opencode-ai/sdk';
|
|
2
|
-
import type { CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor } from './types.js';
|
|
3
|
-
|
|
2
|
+
import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Billing cache version — bump this whenever the persisted `CachedSessionUsage`
|
|
5
|
+
* shape changes in a way that requires recomputation (e.g. new aggregate
|
|
6
|
+
* fields). This is distinct from the plugin *state* version managed by the
|
|
7
|
+
* persistence layer; billing version only governs usage-cache staleness.
|
|
8
|
+
*/
|
|
9
|
+
export declare const USAGE_BILLING_CACHE_VERSION = 4;
|
|
4
10
|
export type ProviderUsage = {
|
|
5
11
|
providerID: string;
|
|
6
12
|
input: number;
|
|
@@ -13,6 +19,7 @@ export type ProviderUsage = {
|
|
|
13
19
|
cost: number;
|
|
14
20
|
apiCost: number;
|
|
15
21
|
assistantMessages: number;
|
|
22
|
+
cacheBuckets?: CacheUsageBuckets;
|
|
16
23
|
};
|
|
17
24
|
export type UsageSummary = {
|
|
18
25
|
input: number;
|
|
@@ -35,14 +42,15 @@ export type UsageOptions = {
|
|
|
35
42
|
/** Cache-behavior classifier for the message model/provider. */
|
|
36
43
|
classifyCacheMode?: (message: AssistantMessage) => CacheCoverageMode;
|
|
37
44
|
};
|
|
38
|
-
export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>):
|
|
39
|
-
|
|
40
|
-
cacheReadCoverage: number | undefined;
|
|
41
|
-
};
|
|
45
|
+
export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
|
|
46
|
+
export declare function getProviderCacheCoverageMetrics(usage: Pick<ProviderUsage, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
|
|
42
47
|
export declare function emptyUsageSummary(): UsageSummary;
|
|
43
48
|
export declare function summarizeMessages(entries: Array<{
|
|
44
49
|
info: Message;
|
|
45
50
|
}>, startAt?: number, sessionCount?: number, options?: UsageOptions): UsageSummary;
|
|
51
|
+
export declare function summarizeMessagesInCompletedRange(entries: Array<{
|
|
52
|
+
info: Message;
|
|
53
|
+
}>, startAt: number, endAt: number, sessionCount?: number, options?: UsageOptions): UsageSummary;
|
|
46
54
|
/**
|
|
47
55
|
* P1: Incremental usage aggregation.
|
|
48
56
|
* Only processes messages newer than the cursor. Returns updated cursor.
|