@leo000001/opencode-quota-sidebar 1.0.2 → 1.2.0
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 +20 -4
- package/dist/cost.js +5 -2
- package/dist/descendants.d.ts +22 -0
- package/dist/descendants.js +78 -0
- package/dist/events.d.ts +8 -0
- package/dist/events.js +31 -0
- package/dist/format.js +127 -23
- package/dist/index.js +190 -631
- package/dist/persistence.d.ts +13 -0
- package/dist/persistence.js +63 -0
- package/dist/providers/core/openai.js +2 -1
- package/dist/providers/third_party/rightcode.js +12 -11
- package/dist/quota.js +18 -8
- package/dist/quota_service.d.ts +23 -0
- package/dist/quota_service.js +188 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +62 -24
- package/dist/storage_chunks.js +74 -1
- package/dist/storage_parse.js +8 -0
- package/dist/storage_paths.d.ts +1 -0
- package/dist/storage_paths.js +12 -4
- package/dist/title.d.ts +5 -0
- package/dist/title.js +26 -2
- package/dist/title_apply.d.ts +33 -0
- package/dist/title_apply.js +189 -0
- package/dist/title_refresh.d.ts +9 -0
- package/dist/title_refresh.js +46 -0
- package/dist/tools.d.ts +56 -0
- package/dist/tools.js +63 -0
- package/dist/types.d.ts +12 -0
- package/dist/usage.js +148 -47
- package/dist/usage_service.d.ts +31 -0
- package/dist/usage_service.js +417 -0
- package/package.json +1 -1
- package/quota-sidebar.config.example.json +5 -1
package/dist/usage.js
CHANGED
|
@@ -13,6 +13,20 @@ export function emptyUsageSummary() {
|
|
|
13
13
|
providers: {},
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
function emptyProviderUsage(providerID) {
|
|
17
|
+
return {
|
|
18
|
+
providerID,
|
|
19
|
+
input: 0,
|
|
20
|
+
output: 0,
|
|
21
|
+
reasoning: 0,
|
|
22
|
+
cacheRead: 0,
|
|
23
|
+
cacheWrite: 0,
|
|
24
|
+
total: 0,
|
|
25
|
+
cost: 0,
|
|
26
|
+
apiCost: 0,
|
|
27
|
+
assistantMessages: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
16
30
|
function isAssistant(message) {
|
|
17
31
|
return message.role === 'assistant';
|
|
18
32
|
}
|
|
@@ -44,18 +58,7 @@ function addMessageUsage(target, message, options) {
|
|
|
44
58
|
target.cost += cost;
|
|
45
59
|
target.apiCost += apiCost;
|
|
46
60
|
const provider = target.providers[message.providerID] ||
|
|
47
|
-
|
|
48
|
-
providerID: message.providerID,
|
|
49
|
-
input: 0,
|
|
50
|
-
output: 0,
|
|
51
|
-
reasoning: 0,
|
|
52
|
-
cacheRead: 0,
|
|
53
|
-
cacheWrite: 0,
|
|
54
|
-
total: 0,
|
|
55
|
-
cost: 0,
|
|
56
|
-
apiCost: 0,
|
|
57
|
-
assistantMessages: 0,
|
|
58
|
-
};
|
|
61
|
+
emptyProviderUsage(message.providerID);
|
|
59
62
|
provider.input += message.tokens.input;
|
|
60
63
|
provider.output += output;
|
|
61
64
|
provider.cacheRead += message.tokens.cache.read;
|
|
@@ -72,7 +75,9 @@ export function summarizeMessages(entries, startAt = 0, sessionCount = 1, option
|
|
|
72
75
|
for (const entry of entries) {
|
|
73
76
|
if (!isAssistant(entry.info))
|
|
74
77
|
continue;
|
|
75
|
-
if (
|
|
78
|
+
if (typeof entry.info.time.completed !== 'number')
|
|
79
|
+
continue;
|
|
80
|
+
if (!Number.isFinite(entry.info.time.completed))
|
|
76
81
|
continue;
|
|
77
82
|
if (entry.info.time.created < startAt)
|
|
78
83
|
continue;
|
|
@@ -87,7 +92,11 @@ export function summarizeMessages(entries, startAt = 0, sessionCount = 1, option
|
|
|
87
92
|
*/
|
|
88
93
|
export function summarizeMessagesIncremental(entries, existingUsage, cursor, forceRescan, options) {
|
|
89
94
|
// If no cursor or force rescan, do full scan
|
|
90
|
-
if (forceRescan ||
|
|
95
|
+
if (forceRescan ||
|
|
96
|
+
!cursor?.lastMessageId ||
|
|
97
|
+
typeof cursor.lastMessageTime !== 'number' ||
|
|
98
|
+
!Number.isFinite(cursor.lastMessageTime) ||
|
|
99
|
+
!existingUsage) {
|
|
91
100
|
const usage = summarizeMessages(entries, 0, 1, options);
|
|
92
101
|
const lastMsg = findLastCompletedAssistant(entries);
|
|
93
102
|
return {
|
|
@@ -95,31 +104,102 @@ export function summarizeMessagesIncremental(entries, existingUsage, cursor, for
|
|
|
95
104
|
cursor: {
|
|
96
105
|
lastMessageId: lastMsg?.id,
|
|
97
106
|
lastMessageTime: lastMsg?.time.completed ?? undefined,
|
|
107
|
+
lastMessageIdsAtTime: lastMsg?.time.completed === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: collectCompletedAssistantIdsAt(entries, lastMsg.time.completed),
|
|
98
110
|
},
|
|
99
111
|
};
|
|
100
112
|
}
|
|
101
|
-
// Incremental: start from existing usage, only process new messages
|
|
113
|
+
// Incremental: start from existing usage, only process new messages.
|
|
114
|
+
// Order-independent: use completed-time cursor (with id tie-breaker).
|
|
102
115
|
const summary = fromCachedSessionUsage(existingUsage, 1);
|
|
116
|
+
const cursorTime = cursor.lastMessageTime;
|
|
117
|
+
const cursorID = cursor.lastMessageId;
|
|
118
|
+
const cursorIDsAtTime = Array.isArray(cursor.lastMessageIdsAtTime)
|
|
119
|
+
? new Set(cursor.lastMessageIdsAtTime)
|
|
120
|
+
: undefined;
|
|
121
|
+
// If the cursor doesn't record ids-at-time, and we see other messages with the
|
|
122
|
+
// same completed timestamp but "earlier" ids, the id tie-breaker can miss
|
|
123
|
+
// newly-arrived messages. Force a full rescan once to initialize ids-at-time.
|
|
124
|
+
if (!cursorIDsAtTime) {
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const msg = entry.info;
|
|
127
|
+
if (!isAssistant(msg))
|
|
128
|
+
continue;
|
|
129
|
+
if (typeof msg.time.completed !== 'number')
|
|
130
|
+
continue;
|
|
131
|
+
if (!Number.isFinite(msg.time.completed))
|
|
132
|
+
continue;
|
|
133
|
+
if (msg.id === cursorID)
|
|
134
|
+
continue;
|
|
135
|
+
if (msg.time.completed === cursorTime &&
|
|
136
|
+
msg.id.localeCompare(cursorID) < 0) {
|
|
137
|
+
const usage = summarizeMessages(entries, 0, 1, options);
|
|
138
|
+
const lastMsg = findLastCompletedAssistant(entries);
|
|
139
|
+
return {
|
|
140
|
+
usage,
|
|
141
|
+
cursor: {
|
|
142
|
+
lastMessageId: lastMsg?.id,
|
|
143
|
+
lastMessageTime: lastMsg?.time.completed ?? undefined,
|
|
144
|
+
lastMessageIdsAtTime: lastMsg?.time.completed === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: collectCompletedAssistantIdsAt(entries, lastMsg.time.completed),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const isAfterCursor = (message) => {
|
|
153
|
+
const completed = message.time.completed;
|
|
154
|
+
if (typeof completed !== 'number' || !Number.isFinite(completed))
|
|
155
|
+
return false;
|
|
156
|
+
if (completed > cursorTime)
|
|
157
|
+
return true;
|
|
158
|
+
if (completed < cursorTime)
|
|
159
|
+
return false;
|
|
160
|
+
if (cursorIDsAtTime) {
|
|
161
|
+
return !cursorIDsAtTime.has(message.id);
|
|
162
|
+
}
|
|
163
|
+
// Same timestamp: best-effort tie-breaker.
|
|
164
|
+
return message.id.localeCompare(cursorID) > 0;
|
|
165
|
+
};
|
|
166
|
+
const newerThan = (left, right) => {
|
|
167
|
+
if (left.time !== right.time)
|
|
168
|
+
return left.time > right.time;
|
|
169
|
+
return left.id.localeCompare(right.id) > 0;
|
|
170
|
+
};
|
|
103
171
|
let foundCursor = false;
|
|
104
|
-
let
|
|
172
|
+
let nextCursor = { ...cursor };
|
|
105
173
|
for (const entry of entries) {
|
|
106
|
-
|
|
174
|
+
const msg = entry.info;
|
|
175
|
+
if (!isAssistant(msg))
|
|
107
176
|
continue;
|
|
108
|
-
if (
|
|
177
|
+
if (typeof msg.time.completed !== 'number')
|
|
109
178
|
continue;
|
|
110
|
-
|
|
111
|
-
if (!foundCursor) {
|
|
112
|
-
if (entry.info.id === cursor.lastMessageId) {
|
|
113
|
-
foundCursor = true;
|
|
114
|
-
}
|
|
179
|
+
if (!Number.isFinite(msg.time.completed))
|
|
115
180
|
continue;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
181
|
+
if (msg.id === cursorID)
|
|
182
|
+
foundCursor = true;
|
|
183
|
+
if (!isAfterCursor(msg))
|
|
184
|
+
continue;
|
|
185
|
+
addMessageUsage(summary, msg, options);
|
|
186
|
+
const candidate = { id: msg.id, time: msg.time.completed };
|
|
187
|
+
const current = {
|
|
188
|
+
id: nextCursor.lastMessageId || cursorID,
|
|
189
|
+
time: nextCursor.lastMessageTime ?? cursorTime,
|
|
122
190
|
};
|
|
191
|
+
if (newerThan(candidate, current)) {
|
|
192
|
+
nextCursor = {
|
|
193
|
+
lastMessageId: msg.id,
|
|
194
|
+
lastMessageTime: msg.time.completed,
|
|
195
|
+
lastMessageIdsAtTime: [msg.id],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
else if (nextCursor.lastMessageTime === msg.time.completed) {
|
|
199
|
+
const ids = new Set(nextCursor.lastMessageIdsAtTime || []);
|
|
200
|
+
ids.add(msg.id);
|
|
201
|
+
nextCursor.lastMessageIdsAtTime = Array.from(ids).sort();
|
|
202
|
+
}
|
|
123
203
|
}
|
|
124
204
|
// If we never found the cursor message, the history may have been modified.
|
|
125
205
|
// Fall back to full rescan.
|
|
@@ -131,18 +211,50 @@ export function summarizeMessagesIncremental(entries, existingUsage, cursor, for
|
|
|
131
211
|
cursor: {
|
|
132
212
|
lastMessageId: lastMsg?.id,
|
|
133
213
|
lastMessageTime: lastMsg?.time.completed ?? undefined,
|
|
214
|
+
lastMessageIdsAtTime: lastMsg?.time.completed === undefined
|
|
215
|
+
? undefined
|
|
216
|
+
: collectCompletedAssistantIdsAt(entries, lastMsg.time.completed),
|
|
134
217
|
},
|
|
135
218
|
};
|
|
136
219
|
}
|
|
137
|
-
return { usage: summary, cursor:
|
|
220
|
+
return { usage: summary, cursor: nextCursor };
|
|
221
|
+
}
|
|
222
|
+
function collectCompletedAssistantIdsAt(entries, completedTime) {
|
|
223
|
+
const ids = [];
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
const msg = entry.info;
|
|
226
|
+
if (!isAssistant(msg))
|
|
227
|
+
continue;
|
|
228
|
+
if (typeof msg.time.completed !== 'number')
|
|
229
|
+
continue;
|
|
230
|
+
if (!Number.isFinite(msg.time.completed))
|
|
231
|
+
continue;
|
|
232
|
+
if (msg.time.completed !== completedTime)
|
|
233
|
+
continue;
|
|
234
|
+
ids.push(msg.id);
|
|
235
|
+
}
|
|
236
|
+
return Array.from(new Set(ids)).sort();
|
|
138
237
|
}
|
|
139
238
|
function findLastCompletedAssistant(entries) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
239
|
+
let best;
|
|
240
|
+
let bestTime = -Infinity;
|
|
241
|
+
let bestID = '';
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
const msg = entry.info;
|
|
244
|
+
if (!isAssistant(msg))
|
|
245
|
+
continue;
|
|
246
|
+
if (typeof msg.time.completed !== 'number')
|
|
247
|
+
continue;
|
|
248
|
+
if (!Number.isFinite(msg.time.completed))
|
|
249
|
+
continue;
|
|
250
|
+
const t = msg.time.completed;
|
|
251
|
+
if (t > bestTime || (t === bestTime && msg.id.localeCompare(bestID) > 0)) {
|
|
252
|
+
best = msg;
|
|
253
|
+
bestTime = t;
|
|
254
|
+
bestID = msg.id;
|
|
255
|
+
}
|
|
144
256
|
}
|
|
145
|
-
return
|
|
257
|
+
return best;
|
|
146
258
|
}
|
|
147
259
|
export function mergeUsage(target, source) {
|
|
148
260
|
target.input += source.input;
|
|
@@ -156,18 +268,7 @@ export function mergeUsage(target, source) {
|
|
|
156
268
|
target.sessionCount += source.sessionCount;
|
|
157
269
|
for (const provider of Object.values(source.providers)) {
|
|
158
270
|
const existing = target.providers[provider.providerID] ||
|
|
159
|
-
|
|
160
|
-
providerID: provider.providerID,
|
|
161
|
-
input: 0,
|
|
162
|
-
output: 0,
|
|
163
|
-
reasoning: 0,
|
|
164
|
-
cacheRead: 0,
|
|
165
|
-
cacheWrite: 0,
|
|
166
|
-
total: 0,
|
|
167
|
-
cost: 0,
|
|
168
|
-
apiCost: 0,
|
|
169
|
-
assistantMessages: 0,
|
|
170
|
-
};
|
|
271
|
+
emptyProviderUsage(provider.providerID);
|
|
171
272
|
existing.input += provider.input;
|
|
172
273
|
existing.output += provider.output;
|
|
173
274
|
existing.cacheRead += provider.cacheRead;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
import { type UsageSummary } from './usage.js';
|
|
3
|
+
import type { QuotaSidebarConfig, QuotaSidebarState } from './types.js';
|
|
4
|
+
type DescendantsResolver = {
|
|
5
|
+
listDescendantSessionIDs: (sessionID: string, opts: {
|
|
6
|
+
maxDepth: number;
|
|
7
|
+
maxSessions: number;
|
|
8
|
+
concurrency: number;
|
|
9
|
+
}) => Promise<string[]>;
|
|
10
|
+
};
|
|
11
|
+
type Persistence = {
|
|
12
|
+
markDirty: (dateKey: string | undefined) => void;
|
|
13
|
+
scheduleSave: () => void;
|
|
14
|
+
flushSave: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function createUsageService(deps: {
|
|
17
|
+
state: QuotaSidebarState;
|
|
18
|
+
config: QuotaSidebarConfig;
|
|
19
|
+
statePath: string;
|
|
20
|
+
client: PluginInput['client'];
|
|
21
|
+
directory: string;
|
|
22
|
+
persistence: Persistence;
|
|
23
|
+
descendantsResolver: DescendantsResolver;
|
|
24
|
+
}): {
|
|
25
|
+
summarizeSessionUsageForDisplay: (sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
|
|
26
|
+
summarizeForTool: (period: "session" | "day" | "week" | "month", sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
|
|
27
|
+
markSessionDirty: (sessionID: string) => void;
|
|
28
|
+
markForceRescan: (sessionID: string) => void;
|
|
29
|
+
forgetSession: (sessionID: string) => void;
|
|
30
|
+
};
|
|
31
|
+
export {};
|