@leo000001/opencode-quota-sidebar 3.0.9 → 4.0.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/CHANGELOG.md +0 -1
- package/README.md +157 -42
- package/README.zh-CN.md +157 -42
- package/SECURITY.md +1 -1
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +354 -0
- package/dist/cli_render.d.ts +17 -0
- package/dist/cli_render.js +292 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +2 -2
- package/dist/format.d.ts +4 -0
- package/dist/format.js +302 -41
- package/dist/history_messages.d.ts +8 -0
- package/dist/history_messages.js +157 -0
- package/dist/history_usage.d.ts +93 -0
- package/dist/history_usage.js +251 -0
- package/dist/index.js +29 -4
- package/dist/period.d.ts +29 -1
- package/dist/period.js +187 -9
- package/dist/provider_catalog.d.ts +8 -0
- package/dist/provider_catalog.js +68 -0
- package/dist/providers/core/anthropic.d.ts +1 -1
- package/dist/providers/core/anthropic.js +69 -45
- package/dist/providers/core/openai.js +101 -8
- package/dist/providers/index.d.ts +1 -2
- package/dist/providers/index.js +1 -3
- package/dist/quota.d.ts +4 -2
- package/dist/quota.js +18 -21
- package/dist/quota_render.d.ts +1 -1
- package/dist/quota_render.js +23 -24
- package/dist/quota_service.d.ts +1 -0
- package/dist/quota_service.js +151 -19
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/storage_dates.d.ts +1 -1
- package/dist/storage_dates.js +8 -5
- package/dist/storage_parse.js +23 -1
- package/dist/supported_quota.d.ts +4 -0
- package/dist/supported_quota.js +36 -0
- package/dist/title.js +18 -8
- package/dist/tools.d.ts +14 -3
- package/dist/tools.js +54 -2
- package/dist/tui.tsx +17 -6
- package/dist/tui_helpers.js +11 -6
- package/dist/types.d.ts +8 -0
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +93 -9
- package/dist/usage_service.d.ts +4 -1
- package/dist/usage_service.js +193 -189
- package/package.json +4 -1
- package/quota-sidebar.config.example.json +36 -45
- package/dist/providers/third_party/xyai.d.ts +0 -2
- package/dist/providers/third_party/xyai.js +0 -348
package/dist/usage_service.js
CHANGED
|
@@ -3,7 +3,9 @@ import { API_COST_ENABLED_PROVIDERS, cacheCoverageModeFromRates, calcEquivalentA
|
|
|
3
3
|
import { deleteSessionFromDayChunk, dateKeyFromTimestamp, scanAllSessions, updateSessionsInDayChunks, } from './storage.js';
|
|
4
4
|
import { periodStart } from './period.js';
|
|
5
5
|
import { debug, debugError, isRecord, mapConcurrent, swallow, } from './helpers.js';
|
|
6
|
-
import { emptyUsageSummary, fromCachedSessionUsage,
|
|
6
|
+
import { accumulateMessagesInCompletedRange, emptyUsageSummary, fromCachedSessionUsage, mergeCursorFromEntries, mergeUsage, summarizeMessagesIncremental, toCachedSessionUsage, USAGE_BILLING_CACHE_VERSION, } from './usage.js';
|
|
7
|
+
import { decodeMessageEntries, isMissingSessionError, nextCursorFromResponse, } from './history_messages.js';
|
|
8
|
+
import { computeHistoryUsage } from './history_usage.js';
|
|
7
9
|
const READ_ONLY_CACHE_PROVIDERS = new Set([
|
|
8
10
|
'openai',
|
|
9
11
|
'github-copilot',
|
|
@@ -135,164 +137,50 @@ export function createUsageService(deps) {
|
|
|
135
137
|
// treat it as read-only (the safer default — avoids overstating cached ratio).
|
|
136
138
|
return 'read-only';
|
|
137
139
|
};
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
input: value.input,
|
|
152
|
-
output: value.output,
|
|
153
|
-
reasoning,
|
|
154
|
-
cache: { read, write },
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
const decodeMessageInfo = (value) => {
|
|
158
|
-
if (!isRecord(value))
|
|
159
|
-
return undefined;
|
|
160
|
-
if (typeof value.id !== 'string')
|
|
161
|
-
return undefined;
|
|
162
|
-
if (typeof value.sessionID !== 'string')
|
|
163
|
-
return undefined;
|
|
164
|
-
if (typeof value.role !== 'string')
|
|
165
|
-
return undefined;
|
|
166
|
-
if (!isRecord(value.time))
|
|
167
|
-
return undefined;
|
|
168
|
-
if (!isFiniteNumber(value.time.created))
|
|
169
|
-
return undefined;
|
|
170
|
-
if (value.time.completed !== undefined &&
|
|
171
|
-
!isFiniteNumber(value.time.completed)) {
|
|
172
|
-
return undefined;
|
|
140
|
+
const loadSessionEntries = async (sessionID) => {
|
|
141
|
+
try {
|
|
142
|
+
const response = await deps.client.session.messages({
|
|
143
|
+
path: { id: sessionID },
|
|
144
|
+
query: { directory: deps.directory },
|
|
145
|
+
throwOnError: true,
|
|
146
|
+
});
|
|
147
|
+
const data = response.data;
|
|
148
|
+
const entries = decodeMessageEntries(data);
|
|
149
|
+
if (!entries)
|
|
150
|
+
return { status: 'error' };
|
|
151
|
+
return { status: 'ok', entries };
|
|
173
152
|
}
|
|
174
|
-
|
|
153
|
+
catch (error) {
|
|
154
|
+
debugError(`loadSessionEntries ${sessionID}`, error);
|
|
175
155
|
return {
|
|
176
|
-
|
|
177
|
-
time: {
|
|
178
|
-
created: value.time.created,
|
|
179
|
-
completed: value.time.completed,
|
|
180
|
-
},
|
|
156
|
+
status: isMissingSessionError(error) ? 'missing' : 'error',
|
|
181
157
|
};
|
|
182
158
|
}
|
|
183
|
-
if (typeof value.providerID !== 'string')
|
|
184
|
-
return undefined;
|
|
185
|
-
if (typeof value.modelID !== 'string')
|
|
186
|
-
return undefined;
|
|
187
|
-
const tokens = decodeTokens(value.tokens);
|
|
188
|
-
if (!tokens)
|
|
189
|
-
return undefined;
|
|
190
|
-
// Normalize token fields to a stable shape (some providers/SDK versions may
|
|
191
|
-
// omit reasoning/cache.write; treat them as 0).
|
|
192
|
-
return {
|
|
193
|
-
...value,
|
|
194
|
-
time: {
|
|
195
|
-
created: value.time.created,
|
|
196
|
-
completed: value.time.completed,
|
|
197
|
-
},
|
|
198
|
-
tokens,
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
|
-
const decodeMessageEntry = (value) => {
|
|
202
|
-
if (!isRecord(value))
|
|
203
|
-
return undefined;
|
|
204
|
-
const decoded = decodeMessageInfo(value.info);
|
|
205
|
-
if (!decoded)
|
|
206
|
-
return undefined;
|
|
207
|
-
return { info: decoded };
|
|
208
|
-
};
|
|
209
|
-
const decodeMessageEntries = (value) => {
|
|
210
|
-
if (!Array.isArray(value))
|
|
211
|
-
return undefined;
|
|
212
|
-
const decoded = value
|
|
213
|
-
.map((item) => decodeMessageEntry(item))
|
|
214
|
-
.filter((item) => Boolean(item));
|
|
215
|
-
if (decoded.length > 0 && decoded.length < value.length) {
|
|
216
|
-
debug(`message entries partially decoded: kept ${decoded.length}/${value.length}`);
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
// If the API returned entries but none match the expected shape,
|
|
220
|
-
// treat it as a load failure so we don't silently undercount.
|
|
221
|
-
if (decoded.length === 0 && value.length > 0)
|
|
222
|
-
return undefined;
|
|
223
|
-
return decoded;
|
|
224
|
-
};
|
|
225
|
-
const errorStatusCode = (value, seen = new Set()) => {
|
|
226
|
-
if (!isRecord(value) || seen.has(value))
|
|
227
|
-
return undefined;
|
|
228
|
-
seen.add(value);
|
|
229
|
-
const status = value.status;
|
|
230
|
-
if (typeof status === 'number' && Number.isFinite(status))
|
|
231
|
-
return status;
|
|
232
|
-
const statusCode = value.statusCode;
|
|
233
|
-
if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
|
|
234
|
-
return statusCode;
|
|
235
|
-
}
|
|
236
|
-
return (errorStatusCode(value.response, seen) ||
|
|
237
|
-
errorStatusCode(value.cause, seen) ||
|
|
238
|
-
errorStatusCode(value.error, seen));
|
|
239
|
-
};
|
|
240
|
-
const errorText = (value, seen = new Set()) => {
|
|
241
|
-
if (!value || seen.has(value))
|
|
242
|
-
return '';
|
|
243
|
-
if (typeof value === 'string')
|
|
244
|
-
return value;
|
|
245
|
-
if (typeof value === 'number' || typeof value === 'boolean')
|
|
246
|
-
return `${value}`;
|
|
247
|
-
if (value instanceof Error) {
|
|
248
|
-
seen.add(value);
|
|
249
|
-
return [
|
|
250
|
-
value.message,
|
|
251
|
-
errorText(value.cause, seen),
|
|
252
|
-
]
|
|
253
|
-
.filter(Boolean)
|
|
254
|
-
.join('\n');
|
|
255
|
-
}
|
|
256
|
-
if (!isRecord(value))
|
|
257
|
-
return '';
|
|
258
|
-
seen.add(value);
|
|
259
|
-
return [
|
|
260
|
-
typeof value.message === 'string' ? value.message : '',
|
|
261
|
-
typeof value.error === 'string' ? value.error : '',
|
|
262
|
-
typeof value.detail === 'string' ? value.detail : '',
|
|
263
|
-
typeof value.title === 'string' ? value.title : '',
|
|
264
|
-
errorText(value.response, seen),
|
|
265
|
-
errorText(value.data, seen),
|
|
266
|
-
errorText(value.cause, seen),
|
|
267
|
-
]
|
|
268
|
-
.filter(Boolean)
|
|
269
|
-
.join('\n');
|
|
270
159
|
};
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
if (status === 404 || status === 410)
|
|
274
|
-
return true;
|
|
275
|
-
const text = errorText(error).toLowerCase();
|
|
276
|
-
if (!text)
|
|
277
|
-
return false;
|
|
278
|
-
return (/\b(session|conversation)\b.*\b(not found|missing|deleted|does not exist)\b/.test(text) ||
|
|
279
|
-
/\b(not found|missing|deleted|does not exist)\b.*\b(session|conversation)\b/.test(text));
|
|
280
|
-
};
|
|
281
|
-
const loadSessionEntries = async (sessionID) => {
|
|
160
|
+
const MESSAGE_PAGE_LIMIT = 200;
|
|
161
|
+
const loadSessionEntriesPage = async (sessionID, before) => {
|
|
282
162
|
try {
|
|
283
163
|
const response = await deps.client.session.messages({
|
|
284
164
|
path: { id: sessionID },
|
|
285
|
-
query: {
|
|
165
|
+
query: {
|
|
166
|
+
directory: deps.directory,
|
|
167
|
+
limit: MESSAGE_PAGE_LIMIT,
|
|
168
|
+
...(before ? { before } : {}),
|
|
169
|
+
},
|
|
286
170
|
throwOnError: true,
|
|
287
171
|
});
|
|
288
172
|
const data = response.data;
|
|
289
173
|
const entries = decodeMessageEntries(data);
|
|
290
174
|
if (!entries)
|
|
291
175
|
return { status: 'error' };
|
|
292
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
status: 'ok',
|
|
178
|
+
entries,
|
|
179
|
+
nextBefore: nextCursorFromResponse(response),
|
|
180
|
+
};
|
|
293
181
|
}
|
|
294
182
|
catch (error) {
|
|
295
|
-
debugError(`
|
|
183
|
+
debugError(`loadSessionEntriesPage ${sessionID}`, error);
|
|
296
184
|
return {
|
|
297
185
|
status: isMissingSessionError(error) ? 'missing' : 'error',
|
|
298
186
|
};
|
|
@@ -349,6 +237,21 @@ export function createUsageService(deps) {
|
|
|
349
237
|
return modelCostLookupKeys(info.providerID, info.modelID).some((key) => Boolean(modelCostMap[key]));
|
|
350
238
|
});
|
|
351
239
|
};
|
|
240
|
+
const shouldTrackFullUsageForRange = (cached, hasPricing) => {
|
|
241
|
+
if (!cached)
|
|
242
|
+
return true;
|
|
243
|
+
if (!isUsageBillingCurrent(cached))
|
|
244
|
+
return true;
|
|
245
|
+
if (!hasPricing)
|
|
246
|
+
return false;
|
|
247
|
+
if (cached.assistantMessages <= 0)
|
|
248
|
+
return false;
|
|
249
|
+
if (cached.apiCost > 0)
|
|
250
|
+
return false;
|
|
251
|
+
if (cached.total <= 0)
|
|
252
|
+
return false;
|
|
253
|
+
return hasAnySubscriptionProvider(cached);
|
|
254
|
+
};
|
|
352
255
|
const summarizeSessionUsage = async (sessionID, generationAtStart, options) => {
|
|
353
256
|
const load = await loadSessionEntries(sessionID);
|
|
354
257
|
const entries = load.status === 'ok' ? load.entries : undefined;
|
|
@@ -487,69 +390,88 @@ export function createUsageService(deps) {
|
|
|
487
390
|
return merged;
|
|
488
391
|
};
|
|
489
392
|
const RANGE_USAGE_CONCURRENCY = 5;
|
|
393
|
+
const filterRangeSessions = (sessions, startAt, endAt) => {
|
|
394
|
+
return sessions.filter((session) => {
|
|
395
|
+
if (session.state.createdAt > endAt)
|
|
396
|
+
return false;
|
|
397
|
+
if (session.state.dirty === true)
|
|
398
|
+
return true;
|
|
399
|
+
const lastMessageTime = session.state.cursor?.lastMessageTime;
|
|
400
|
+
if (typeof lastMessageTime === 'number' &&
|
|
401
|
+
Number.isFinite(lastMessageTime) &&
|
|
402
|
+
lastMessageTime < startAt) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
});
|
|
407
|
+
};
|
|
490
408
|
const summarizeRangeUsage = async (period) => {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
const startAt = periodStart(period, now);
|
|
411
|
+
const endAt = now;
|
|
493
412
|
await deps.persistence.flushSave();
|
|
494
|
-
const sessions = await scanAllSessions(deps.statePath, deps.state);
|
|
413
|
+
const sessions = filterRangeSessions(await scanAllSessions(deps.statePath, deps.state), startAt, endAt);
|
|
495
414
|
const usage = emptyUsageSummary();
|
|
496
415
|
const modelCostMap = await getModelCostMap();
|
|
497
416
|
const hasPricing = Object.keys(modelCostMap).length > 0;
|
|
498
417
|
if (sessions.length > 0) {
|
|
499
418
|
const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
500
|
-
const
|
|
501
|
-
if (load.status !== 'ok') {
|
|
502
|
-
return {
|
|
503
|
-
sessionID: session.sessionID,
|
|
504
|
-
dateKey: session.dateKey,
|
|
505
|
-
createdAt: session.state.createdAt,
|
|
506
|
-
lastMessageTime: session.state.cursor?.lastMessageTime,
|
|
507
|
-
dirty: session.state.dirty === true,
|
|
508
|
-
computed: emptyUsageSummary(),
|
|
509
|
-
fullUsage: undefined,
|
|
510
|
-
loadFailed: load.status === 'error',
|
|
511
|
-
missing: load.status === 'missing',
|
|
512
|
-
persist: false,
|
|
513
|
-
cursor: undefined,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
const entries = load.entries;
|
|
517
|
-
const computed = summarizeMessagesInCompletedRange(entries, startAt, endAt, 0, {
|
|
419
|
+
const usageOptions = {
|
|
518
420
|
calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
|
|
519
421
|
classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
|
|
520
|
-
}
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
422
|
+
};
|
|
423
|
+
const computed = emptyUsageSummary();
|
|
424
|
+
const trackFullUsage = shouldTrackFullUsageForRange(session.state.usage, hasPricing);
|
|
425
|
+
const fullUsage = trackFullUsage ? emptyUsageSummary() : undefined;
|
|
426
|
+
let cursor;
|
|
427
|
+
let hasResolvableApiCostMessage = false;
|
|
428
|
+
let before;
|
|
429
|
+
while (true) {
|
|
430
|
+
const load = await loadSessionEntriesPage(session.sessionID, before);
|
|
431
|
+
if (load.status !== 'ok') {
|
|
432
|
+
return {
|
|
433
|
+
sessionID: session.sessionID,
|
|
434
|
+
dateKey: session.dateKey,
|
|
435
|
+
createdAt: session.state.createdAt,
|
|
436
|
+
lastMessageTime: session.state.cursor?.lastMessageTime,
|
|
437
|
+
dirty: session.state.dirty === true,
|
|
438
|
+
computed: emptyUsageSummary(),
|
|
439
|
+
fullUsage: undefined,
|
|
440
|
+
loadFailed: load.status === 'error',
|
|
441
|
+
missing: load.status === 'missing',
|
|
442
|
+
persist: false,
|
|
443
|
+
cursor: undefined,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const entries = load.entries;
|
|
447
|
+
if (entries.length === 0)
|
|
448
|
+
break;
|
|
449
|
+
accumulateMessagesInCompletedRange(computed, entries, startAt, endAt, usageOptions);
|
|
450
|
+
if (fullUsage) {
|
|
451
|
+
accumulateMessagesInCompletedRange(fullUsage, entries, 0, Number.POSITIVE_INFINITY, usageOptions);
|
|
452
|
+
cursor = mergeCursorFromEntries(cursor, entries);
|
|
453
|
+
}
|
|
454
|
+
if (!hasResolvableApiCostMessage) {
|
|
455
|
+
hasResolvableApiCostMessage = hasResolvableApiCostMessages(entries, modelCostMap);
|
|
456
|
+
}
|
|
457
|
+
if (!load.nextBefore)
|
|
458
|
+
break;
|
|
459
|
+
before = load.nextBefore;
|
|
537
460
|
}
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
});
|
|
461
|
+
const shouldPersistFullUsage = !!fullUsage &&
|
|
462
|
+
(!session.state.usage ||
|
|
463
|
+
shouldRecomputeUsageCache(session.state.usage, hasPricing, hasResolvableApiCostMessage));
|
|
542
464
|
return {
|
|
543
465
|
sessionID: session.sessionID,
|
|
544
466
|
dateKey: session.dateKey,
|
|
545
467
|
createdAt: session.state.createdAt,
|
|
546
|
-
lastMessageTime: cursor
|
|
547
|
-
dirty:
|
|
468
|
+
lastMessageTime: cursor?.lastMessageTime,
|
|
469
|
+
dirty: session.state.dirty === true,
|
|
548
470
|
computed,
|
|
549
|
-
fullUsage,
|
|
471
|
+
fullUsage: shouldPersistFullUsage ? fullUsage : undefined,
|
|
550
472
|
loadFailed: false,
|
|
551
473
|
missing: false,
|
|
552
|
-
persist:
|
|
474
|
+
persist: shouldPersistFullUsage,
|
|
553
475
|
cursor,
|
|
554
476
|
};
|
|
555
477
|
});
|
|
@@ -632,6 +554,87 @@ export function createUsageService(deps) {
|
|
|
632
554
|
}
|
|
633
555
|
return usage;
|
|
634
556
|
};
|
|
557
|
+
const summarizeHistoryUsage = async (period, rawSince) => {
|
|
558
|
+
await deps.persistence.flushSave();
|
|
559
|
+
const sessions = await scanAllSessions(deps.statePath, deps.state);
|
|
560
|
+
const result = await computeHistoryUsage({
|
|
561
|
+
sessions,
|
|
562
|
+
loadMessagesPage: loadSessionEntriesPage,
|
|
563
|
+
getModelCostMap: getModelCostMap,
|
|
564
|
+
calcApiCost: (message, costMap) => calcEquivalentApiCost(message, costMap),
|
|
565
|
+
classifyCacheMode: (message, costMap) => classifyCacheMode(message, costMap),
|
|
566
|
+
hasResolvableApiCostMessages: (entries, costMap) => hasResolvableApiCostMessages(entries, costMap),
|
|
567
|
+
shouldTrackFullUsage: shouldTrackFullUsageForRange,
|
|
568
|
+
shouldRecomputeUsageCache,
|
|
569
|
+
throwOnLoadFailure: true,
|
|
570
|
+
}, period, rawSince);
|
|
571
|
+
// Server-side persistence: persist recomputed full-session usage back to
|
|
572
|
+
// memory state and day chunks so future queries are faster.
|
|
573
|
+
const hints = result.persistenceHints;
|
|
574
|
+
if (hints && hints.length > 0) {
|
|
575
|
+
const missingSessions = hints.filter((item) => item.missing);
|
|
576
|
+
if (missingSessions.length > 0) {
|
|
577
|
+
let stateChanged = false;
|
|
578
|
+
for (const missing of missingSessions) {
|
|
579
|
+
deps.state.deletedSessionDateMap[missing.sessionID] = missing.dateKey;
|
|
580
|
+
delete deps.state.sessions[missing.sessionID];
|
|
581
|
+
delete deps.state.sessionDateMap[missing.sessionID];
|
|
582
|
+
deps.persistence.markDirty(missing.dateKey);
|
|
583
|
+
forgetSession(missing.sessionID);
|
|
584
|
+
stateChanged = true;
|
|
585
|
+
}
|
|
586
|
+
await Promise.all(missingSessions.map(async (missing) => {
|
|
587
|
+
const deletedFromChunk = await deleteSessionFromDayChunk(deps.statePath, missing.sessionID, missing.dateKey).catch((error) => {
|
|
588
|
+
swallow('deleteSessionFromDayChunk')(error);
|
|
589
|
+
return false;
|
|
590
|
+
});
|
|
591
|
+
if (!deletedFromChunk)
|
|
592
|
+
return;
|
|
593
|
+
delete deps.state.deletedSessionDateMap[missing.sessionID];
|
|
594
|
+
stateChanged = true;
|
|
595
|
+
}));
|
|
596
|
+
if (stateChanged)
|
|
597
|
+
deps.persistence.scheduleSave();
|
|
598
|
+
}
|
|
599
|
+
let dirty = false;
|
|
600
|
+
const diskOnlyUpdates = [];
|
|
601
|
+
for (const item of hints) {
|
|
602
|
+
if (!item.persist || !item.fullUsage)
|
|
603
|
+
continue;
|
|
604
|
+
const memoryState = deps.state.sessions[item.sessionID];
|
|
605
|
+
if (memoryState) {
|
|
606
|
+
memoryState.usage = toCachedSessionUsage(item.fullUsage);
|
|
607
|
+
memoryState.cursor = item.cursor;
|
|
608
|
+
const resolvedDateKey = deps.state.sessionDateMap[item.sessionID] ||
|
|
609
|
+
dateKeyFromTimestamp(memoryState.createdAt);
|
|
610
|
+
deps.state.sessionDateMap[item.sessionID] = resolvedDateKey;
|
|
611
|
+
deps.persistence.markDirty(resolvedDateKey);
|
|
612
|
+
memoryState.dirty = false;
|
|
613
|
+
dirty = true;
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
diskOnlyUpdates.push({
|
|
617
|
+
sessionID: item.sessionID,
|
|
618
|
+
dateKey: item.dateKey,
|
|
619
|
+
usage: toCachedSessionUsage(item.fullUsage),
|
|
620
|
+
cursor: item.cursor,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (diskOnlyUpdates.length > 0) {
|
|
625
|
+
const persisted = await updateSessionsInDayChunks(deps.statePath, diskOnlyUpdates).catch((error) => {
|
|
626
|
+
swallow('updateSessionsInDayChunks')(error);
|
|
627
|
+
return false;
|
|
628
|
+
});
|
|
629
|
+
if (!persisted) {
|
|
630
|
+
throw new Error(`history usage unavailable: failed to persist ${diskOnlyUpdates.length} disk-only session(s)`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (dirty)
|
|
634
|
+
deps.persistence.scheduleSave();
|
|
635
|
+
}
|
|
636
|
+
return result;
|
|
637
|
+
};
|
|
635
638
|
const summarizeForTool = async (period, sessionID, includeChildren) => {
|
|
636
639
|
if (period === 'session') {
|
|
637
640
|
if (!includeChildren) {
|
|
@@ -683,6 +686,7 @@ export function createUsageService(deps) {
|
|
|
683
686
|
return {
|
|
684
687
|
summarizeSessionUsageForDisplay,
|
|
685
688
|
summarizeForTool,
|
|
689
|
+
summarizeHistoryUsage,
|
|
686
690
|
markSessionDirty,
|
|
687
691
|
markForceRescan,
|
|
688
692
|
forgetSession,
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leo000001/opencode-quota-sidebar",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"opencode-quota": "./dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"types": "dist/index.d.ts",
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
@@ -1,45 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"sidebar": {
|
|
3
|
-
"enabled": true,
|
|
4
|
-
"width": 36,
|
|
5
|
-
"titleMode": "auto",
|
|
6
|
-
"multilineTitle": true,
|
|
7
|
-
"showCost": true,
|
|
8
|
-
"showQuota": true,
|
|
9
|
-
"wrapQuotaLines": true,
|
|
10
|
-
"includeChildren": true,
|
|
11
|
-
"childrenMaxDepth": 6,
|
|
12
|
-
"childrenMaxSessions": 128,
|
|
13
|
-
"childrenConcurrency": 5,
|
|
14
|
-
"desktopCompact": {
|
|
15
|
-
"recentRequests": 50,
|
|
16
|
-
"recentMinutes": 60
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"quota": {
|
|
20
|
-
"refreshMs":
|
|
21
|
-
"includeOpenAI": true,
|
|
22
|
-
"includeCopilot": true,
|
|
23
|
-
"includeAnthropic": true,
|
|
24
|
-
"providers": {
|
|
25
|
-
"rightcode": {
|
|
26
|
-
"enabled": true
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
"refreshAccessToken": false,
|
|
39
|
-
"requestTimeoutMs": 8000
|
|
40
|
-
},
|
|
41
|
-
"toast": {
|
|
42
|
-
"durationMs": 12000
|
|
43
|
-
},
|
|
44
|
-
"retentionDays": 730
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"sidebar": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"width": 36,
|
|
5
|
+
"titleMode": "auto",
|
|
6
|
+
"multilineTitle": true,
|
|
7
|
+
"showCost": true,
|
|
8
|
+
"showQuota": true,
|
|
9
|
+
"wrapQuotaLines": true,
|
|
10
|
+
"includeChildren": true,
|
|
11
|
+
"childrenMaxDepth": 6,
|
|
12
|
+
"childrenMaxSessions": 128,
|
|
13
|
+
"childrenConcurrency": 5,
|
|
14
|
+
"desktopCompact": {
|
|
15
|
+
"recentRequests": 50,
|
|
16
|
+
"recentMinutes": 60
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"quota": {
|
|
20
|
+
"refreshMs": 60000,
|
|
21
|
+
"includeOpenAI": true,
|
|
22
|
+
"includeCopilot": true,
|
|
23
|
+
"includeAnthropic": true,
|
|
24
|
+
"providers": {
|
|
25
|
+
"rightcode": {
|
|
26
|
+
"enabled": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"refreshAccessToken": false,
|
|
30
|
+
"requestTimeoutMs": 12000
|
|
31
|
+
},
|
|
32
|
+
"toast": {
|
|
33
|
+
"durationMs": 12000
|
|
34
|
+
},
|
|
35
|
+
"retentionDays": 730
|
|
36
|
+
}
|