@glaicer/supercode-token-usage-panel 0.1.3 → 1.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/README.md +10 -10
- package/dist/usage-model.js +304 -202
- package/dist/usage-panel.js +28 -30
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -9,29 +9,29 @@ An OpenCode plugin that adds a collapsible `Token Usage` section to the TUI sess
|
|
|
9
9
|
|
|
10
10
|
Totals fold in the whole session family: the parent session plus all subagent descendants, from OpenCode's own `session.tokens` / `session.cost` aggregates. When descendants contribute, their usage sums up with parent agent usage.
|
|
11
11
|
|
|
12
|
+
Requires OpenCode v2 (`>=2.0.0`). On OpenCode v1 stay on `@glaicer/supercode-token-usage-panel@0.1.4`.
|
|
13
|
+
|
|
12
14
|
## Install
|
|
13
15
|
|
|
14
|
-
Install with the OpenCode CLI — it
|
|
16
|
+
Install with the OpenCode CLI — it installs the package and registers the plugin in the global CLI configuration (`~/.config/opencode/cli.json`):
|
|
15
17
|
|
|
16
18
|
```bash
|
|
17
|
-
opencode plugin @glaicer/supercode-token-usage-panel
|
|
19
|
+
opencode plugin add @glaicer/supercode-token-usage-panel
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
Restart OpenCode after installing.
|
|
23
|
+
|
|
24
|
+
> [!IMPORTANT]
|
|
25
|
+
> **The first OpenCode load after installing this plugin may be slow.** That's OpenCode downloading the plugin's packages and managed tools into its cache — it happens once. Every subsequent start is fast.
|
|
23
26
|
|
|
24
|
-
Manual install also works: add the package to the `
|
|
27
|
+
Manual install also works: add the package to the `plugins` array in `~/.config/opencode/cli.json`:
|
|
25
28
|
|
|
26
29
|
```jsonc
|
|
27
30
|
{
|
|
28
|
-
"
|
|
31
|
+
"plugins": ["@glaicer/supercode-token-usage-panel"]
|
|
29
32
|
}
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
> [!IMPORTANT]
|
|
33
|
-
> **The first OpenCode load after installing this plugin may be slow.** That's OpenCode downloading the plugin's packages and managed tools into its cache — it happens once. Every subsequent start is fast.
|
|
34
|
-
|
|
35
35
|
## Development
|
|
36
36
|
|
|
37
37
|
```bash
|
package/dist/usage-model.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Reads OpenCode's authoritative session aggregate and family message history,
|
|
5
5
|
* folds completed steps/speed/TTFT, and estimates the current visible stream from
|
|
6
6
|
* deltas. Exposes only ready-to-render rows plus a state flag.
|
|
7
|
+
*
|
|
8
|
+
* OpenCode v2 shape: one assistant message = one model step, so per-step tokens
|
|
9
|
+
* live on `message.tokens` (there are no `step-finish` parts) and TTFT is
|
|
10
|
+
* `message.time.streamed - message.time.created` (`time.streamed` is set by
|
|
11
|
+
* `session.step.streamed`, the first streamed token).
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
export const USAGE_SECTION_TITLE = "Token Usage";
|
|
@@ -18,6 +23,13 @@ export const USAGE_STATUS_TEXT = {
|
|
|
18
23
|
|
|
19
24
|
/** Placeholder for missing values: never render NaN/Infinity as a number. */
|
|
20
25
|
export const USAGE_DASH = "–";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The slice of the TUI plugin context the model reads. The panel passes the
|
|
29
|
+
* real `Plugin.Context` (typecheck at the call site keeps this slice honest);
|
|
30
|
+
* tests fake exactly this surface.
|
|
31
|
+
*/
|
|
32
|
+
|
|
21
33
|
function emptyMetrics() {
|
|
22
34
|
return {
|
|
23
35
|
generated: 0,
|
|
@@ -34,17 +46,20 @@ function allZero(totals) {
|
|
|
34
46
|
function safe(value) {
|
|
35
47
|
return Number.isFinite(value) ? Math.max(0, value ?? 0) : 0;
|
|
36
48
|
}
|
|
37
|
-
function
|
|
38
|
-
if (!session?.tokens) return undefined;
|
|
49
|
+
function totalsFromUsage(tokens, cost) {
|
|
39
50
|
return {
|
|
40
|
-
input: safe(
|
|
41
|
-
output: safe(
|
|
42
|
-
reasoning: safe(
|
|
43
|
-
cacheRead: safe(
|
|
44
|
-
cacheWrite: safe(
|
|
45
|
-
cost: safe(
|
|
51
|
+
input: safe(tokens?.input),
|
|
52
|
+
output: safe(tokens?.output),
|
|
53
|
+
reasoning: safe(tokens?.reasoning),
|
|
54
|
+
cacheRead: safe(tokens?.cache.read),
|
|
55
|
+
cacheWrite: safe(tokens?.cache.write),
|
|
56
|
+
cost: safe(cost)
|
|
46
57
|
};
|
|
47
58
|
}
|
|
59
|
+
function totalsFromSession(session) {
|
|
60
|
+
if (!session?.tokens) return undefined;
|
|
61
|
+
return totalsFromUsage(session.tokens, session.cost);
|
|
62
|
+
}
|
|
48
63
|
function groupDigits(value) {
|
|
49
64
|
return String(Math.trunc(value)).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
50
65
|
}
|
|
@@ -115,7 +130,7 @@ function buildUsageRows(totals, metrics) {
|
|
|
115
130
|
}];
|
|
116
131
|
}
|
|
117
132
|
function formatLiveSpeed(live) {
|
|
118
|
-
if (!live.hasTicked) return
|
|
133
|
+
if (!live.hasTicked) return USAGE_DASH;
|
|
119
134
|
const elapsed = (live.now - live.startedAt) / 1_000;
|
|
120
135
|
const value = live.displayedChars / live.charsPerToken / elapsed;
|
|
121
136
|
const rounded = Math.round(value);
|
|
@@ -138,7 +153,7 @@ function codePoints(value) {
|
|
|
138
153
|
return Array.from(value).length;
|
|
139
154
|
}
|
|
140
155
|
function modelKey(message) {
|
|
141
|
-
return JSON.stringify([message.providerID, message.
|
|
156
|
+
return JSON.stringify([message.model.providerID, message.model.id]);
|
|
142
157
|
}
|
|
143
158
|
function addCalibration(calibrations, key, chars, tokens) {
|
|
144
159
|
if (!positive(chars) || !positive(tokens)) return;
|
|
@@ -158,56 +173,49 @@ function addTotals(target, source) {
|
|
|
158
173
|
target.cacheWrite += source.cacheWrite;
|
|
159
174
|
target.cost += source.cost;
|
|
160
175
|
}
|
|
176
|
+
|
|
177
|
+
/** Minimal identity of a session announced by an event; totals arrive via fetch. */
|
|
178
|
+
|
|
179
|
+
/** A step is finished once its tokens or completion time is recorded. */
|
|
180
|
+
function finishedStep(message) {
|
|
181
|
+
return message.tokens !== undefined || message.time.completed !== undefined;
|
|
182
|
+
}
|
|
161
183
|
function completedMetrics(messages) {
|
|
162
184
|
const result = emptyMetrics();
|
|
163
|
-
for (const {
|
|
164
|
-
info
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (info.role !== "assistant") continue;
|
|
168
|
-
for (const part of parts) {
|
|
169
|
-
if (part.type === "step-finish") result.steps++;
|
|
170
|
-
}
|
|
171
|
-
const message = info;
|
|
172
|
-
if (!positive(message.time.completed ?? 0)) continue;
|
|
185
|
+
for (const info of messages) {
|
|
186
|
+
if (info.type !== "assistant") continue;
|
|
187
|
+
if (finishedStep(info)) result.steps++;
|
|
188
|
+
if (!positive(info.time.completed ?? 0)) continue;
|
|
173
189
|
let stepChars = 0;
|
|
174
190
|
let stepHasTool = false;
|
|
175
|
-
for (const
|
|
176
|
-
if (
|
|
177
|
-
|
|
191
|
+
for (const item of info.content) {
|
|
192
|
+
if (item.type === "text" || item.type === "reasoning") {
|
|
193
|
+
stepChars += codePoints(item.text);
|
|
178
194
|
continue;
|
|
179
195
|
}
|
|
180
|
-
if (
|
|
181
|
-
stepHasTool = true;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (part.type !== "step-finish") continue;
|
|
185
|
-
const tokens = part.tokens.output + part.tokens.reasoning;
|
|
186
|
-
if (!stepHasTool) addCalibration(result.calibrations, modelKey(message), stepChars, tokens);
|
|
187
|
-
stepChars = 0;
|
|
188
|
-
stepHasTool = false;
|
|
196
|
+
if (item.type === "tool") stepHasTool = true;
|
|
189
197
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
+
if (info.tokens && !stepHasTool) {
|
|
199
|
+
addCalibration(result.calibrations, modelKey(info), stepChars, info.tokens.output + info.tokens.reasoning);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Without streamed timing there is no first-token sample and no decode
|
|
203
|
+
// baseline, so the step contributes steps/calibration only.
|
|
204
|
+
const streamed = info.time.streamed;
|
|
205
|
+
if (!positive(streamed ?? 0)) continue;
|
|
206
|
+
const visible = info.content.some(item => item.type === "text" || item.type === "reasoning");
|
|
207
|
+
if (!visible) continue;
|
|
208
|
+
const ttft = streamed - info.time.created;
|
|
198
209
|
if (!positive(ttft)) continue;
|
|
199
210
|
result.ttftMs += ttft;
|
|
200
211
|
result.ttftCount++;
|
|
201
|
-
const generated =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const tools = parts.reduce((sum, part) => {
|
|
206
|
-
if (part.type !== "tool" || part.state.status !== "completed") return sum;
|
|
207
|
-
const duration = part.state.time.end - part.state.time.start;
|
|
212
|
+
const generated = info.tokens ? info.tokens.output + info.tokens.reasoning : 0;
|
|
213
|
+
const tools = info.content.reduce((sum, item) => {
|
|
214
|
+
if (item.type !== "tool" || item.state.status !== "completed") return sum;
|
|
215
|
+
const duration = (item.time.completed ?? 0) - (item.time.ran ?? item.time.created);
|
|
208
216
|
return positive(duration) ? sum + duration : sum;
|
|
209
217
|
}, 0);
|
|
210
|
-
const decode =
|
|
218
|
+
const decode = info.time.completed - info.time.created - ttft - tools;
|
|
211
219
|
if (!positive(generated) || !positive(decode)) continue;
|
|
212
220
|
result.generated += generated;
|
|
213
221
|
result.decodeMs += decode;
|
|
@@ -224,7 +232,21 @@ function addMetrics(target, source) {
|
|
|
224
232
|
addCalibration(target.calibrations, key, sourceCalibration.chars, sourceCalibration.tokens);
|
|
225
233
|
}
|
|
226
234
|
}
|
|
227
|
-
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Whether the viewed session launched subagents: at least one other member of
|
|
238
|
+
* the family descends from it. Siblings and ancestors do not count, so the
|
|
239
|
+
* section title stays "Token Usage" unless the current session itself has
|
|
240
|
+
* descendants.
|
|
241
|
+
*/
|
|
242
|
+
function hasCurrentDescendant(viewedID, contributions) {
|
|
243
|
+
for (const id of contributions.keys()) {
|
|
244
|
+
if (id === viewedID) continue;
|
|
245
|
+
if (belongsToBranch(id, viewedID, contributions)) return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
function aggregateContributions(contributions, incompleteBranches = new Set(), viewedID) {
|
|
228
250
|
let totals;
|
|
229
251
|
const metrics = emptyMetrics();
|
|
230
252
|
for (const contribution of contributions.values()) {
|
|
@@ -239,21 +261,31 @@ function aggregateContributions(contributions, incompleteBranches = new Set()) {
|
|
|
239
261
|
return {
|
|
240
262
|
totals,
|
|
241
263
|
metrics,
|
|
242
|
-
hasDescendants: members.size > 1,
|
|
264
|
+
hasDescendants: viewedID ? hasCurrentDescendant(viewedID, contributions) : members.size > 1,
|
|
243
265
|
members,
|
|
244
266
|
contributions,
|
|
245
267
|
incompleteBranches
|
|
246
268
|
};
|
|
247
269
|
}
|
|
270
|
+
async function listAllMessages(client, sessionID) {
|
|
271
|
+
const collected = [];
|
|
272
|
+
let cursor;
|
|
273
|
+
do {
|
|
274
|
+
const page = await client.message.list({
|
|
275
|
+
sessionID,
|
|
276
|
+
...(cursor ? {
|
|
277
|
+
cursor
|
|
278
|
+
} : {})
|
|
279
|
+
});
|
|
280
|
+
collected.push(...page.data);
|
|
281
|
+
cursor = page.cursor?.next ?? undefined;
|
|
282
|
+
} while (cursor);
|
|
283
|
+
return collected.sort((a, b) => (a.time.created ?? 0) - (b.time.created ?? 0));
|
|
284
|
+
}
|
|
248
285
|
async function fetchContribution(client, session) {
|
|
249
286
|
let metrics = emptyMetrics();
|
|
250
287
|
try {
|
|
251
|
-
|
|
252
|
-
sessionID: session.id
|
|
253
|
-
}, {
|
|
254
|
-
throwOnError: true
|
|
255
|
-
})).data;
|
|
256
|
-
metrics = completedMetrics(messages);
|
|
288
|
+
metrics = completedMetrics(await listAllMessages(client, session.id));
|
|
257
289
|
} catch {
|
|
258
290
|
// Totals stay usable when diagnostic history cannot be read.
|
|
259
291
|
}
|
|
@@ -276,10 +308,8 @@ async function fetchBranch(client, root) {
|
|
|
276
308
|
visited.add(session.id);
|
|
277
309
|
contributions.set(session.id, await fetchContribution(client, session));
|
|
278
310
|
try {
|
|
279
|
-
const children = (await client.session.
|
|
280
|
-
|
|
281
|
-
}, {
|
|
282
|
-
throwOnError: true
|
|
311
|
+
const children = (await client.session.list({
|
|
312
|
+
parentID: session.id
|
|
283
313
|
})).data;
|
|
284
314
|
for (const child of children) {
|
|
285
315
|
if (child && !visited.has(child.id)) queue.push(child);
|
|
@@ -288,7 +318,7 @@ async function fetchBranch(client, root) {
|
|
|
288
318
|
incompleteBranches.add(session.id);
|
|
289
319
|
}
|
|
290
320
|
}
|
|
291
|
-
return aggregateContributions(contributions, incompleteBranches);
|
|
321
|
+
return aggregateContributions(contributions, incompleteBranches, root.id);
|
|
292
322
|
}
|
|
293
323
|
function walkParents(sessionID, contributions, matches) {
|
|
294
324
|
const visited = new Set();
|
|
@@ -308,25 +338,21 @@ function belongsToBranch(sessionID, rootID, contributions) {
|
|
|
308
338
|
}
|
|
309
339
|
|
|
310
340
|
/**
|
|
311
|
-
* Totals cover the
|
|
312
|
-
* The walk counts each session once and keeps
|
|
313
|
-
* branch fails to resolve.
|
|
341
|
+
* Totals cover the whole family resolved from the root: the root session plus
|
|
342
|
+
* all descendants (subagents). The walk counts each session once and keeps
|
|
343
|
+
* resolved aggregates when a branch fails to resolve.
|
|
314
344
|
*/
|
|
315
345
|
async function fetchFamily(client, sessionID) {
|
|
316
|
-
let root =
|
|
346
|
+
let root = await client.session.get({
|
|
317
347
|
sessionID
|
|
318
|
-
}
|
|
319
|
-
throwOnError: true
|
|
320
|
-
})).data;
|
|
348
|
+
});
|
|
321
349
|
if (!root) throw new Error("session unavailable");
|
|
322
350
|
const ancestors = new Set([root.id]);
|
|
323
351
|
while (root.parentID && !ancestors.has(root.parentID)) {
|
|
324
352
|
ancestors.add(root.parentID);
|
|
325
|
-
const parent =
|
|
353
|
+
const parent = await client.session.get({
|
|
326
354
|
sessionID: root.parentID
|
|
327
|
-
}
|
|
328
|
-
throwOnError: true
|
|
329
|
-
})).data;
|
|
355
|
+
});
|
|
330
356
|
if (!parent) throw new Error("parent session unavailable");
|
|
331
357
|
root = parent;
|
|
332
358
|
}
|
|
@@ -345,9 +371,9 @@ async function fetchFamily(client, sessionID) {
|
|
|
345
371
|
*/
|
|
346
372
|
|
|
347
373
|
/**
|
|
348
|
-
* Usage Model over OpenCode's session aggregate. Totals cover the
|
|
349
|
-
* session plus all
|
|
350
|
-
* slower responses from overwriting newer ones.
|
|
374
|
+
* Usage Model over OpenCode's session aggregate. Totals cover the whole family
|
|
375
|
+
* resolved from the root (the session plus all subagent descendants); request
|
|
376
|
+
* sequencing keeps slower responses from overwriting newer ones.
|
|
351
377
|
*/
|
|
352
378
|
export function createUsageModel(api, sessionId, solid) {
|
|
353
379
|
const [remote, setRemote] = solid.createSignal();
|
|
@@ -361,10 +387,14 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
361
387
|
const contributionRevisions = new Map();
|
|
362
388
|
const pendingBranches = new Map();
|
|
363
389
|
const failedMembers = new Set();
|
|
390
|
+
/** Parent links announced by events, for contributions created before any fetch lands. */
|
|
391
|
+
const parents = new Map();
|
|
364
392
|
/** Deleted session ids; a full refresh must never resurrect their totals. */
|
|
365
393
|
const tombstones = new Set();
|
|
394
|
+
/** Content items whose stream already ended; stale deltas must not restart them. */
|
|
395
|
+
const endedParts = new Set();
|
|
366
396
|
let timer;
|
|
367
|
-
let
|
|
397
|
+
let ttftTurnShown = false;
|
|
368
398
|
let members = new Set();
|
|
369
399
|
const isAttachable = session => !!session.parentID && members.has(session.parentID) && !members.has(session.id);
|
|
370
400
|
const captureFreshness = () => ({
|
|
@@ -380,8 +410,11 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
380
410
|
for (const id of ids) contributionRevisions.set(id, revision);
|
|
381
411
|
};
|
|
382
412
|
const publishFamily = (sessionID, contributions, incompleteBranches) => {
|
|
383
|
-
const aggregate = aggregateContributions(contributions, incompleteBranches);
|
|
413
|
+
const aggregate = aggregateContributions(contributions, incompleteBranches, sessionID);
|
|
384
414
|
members = aggregate.members;
|
|
415
|
+
for (const [id, contribution] of contributions) {
|
|
416
|
+
if (contribution.parentID) parents.set(id, contribution.parentID);
|
|
417
|
+
}
|
|
385
418
|
return {
|
|
386
419
|
sessionID,
|
|
387
420
|
totals: aggregate.totals,
|
|
@@ -428,16 +461,37 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
428
461
|
if (timer) clearInterval(timer);
|
|
429
462
|
timer = undefined;
|
|
430
463
|
};
|
|
431
|
-
|
|
432
|
-
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Live TTFT belongs to the first assistant step of a turn only; later steps
|
|
467
|
+
* follow tool calls and would report tool latency as "time to first token".
|
|
468
|
+
* A turn is the run of assistant messages after the last non-assistant
|
|
469
|
+
* message; it is already spoken for when any of its steps finished. When the
|
|
470
|
+
* panel mounts mid-first-step the seeded measurement keeps the elapsed time
|
|
471
|
+
* visible from the step's start.
|
|
472
|
+
*/
|
|
473
|
+
const scanTurnState = sessionID => {
|
|
433
474
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
475
|
+
const messages = api.data.session.message.list(sessionID);
|
|
476
|
+
const run = [];
|
|
477
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
478
|
+
const message = messages[index];
|
|
479
|
+
if (!message || message.type !== "assistant") break;
|
|
480
|
+
run.unshift(message);
|
|
438
481
|
}
|
|
439
|
-
|
|
440
|
-
|
|
482
|
+
const shown = run.some(finishedStep);
|
|
483
|
+
const seed = !shown ? run[0] : undefined;
|
|
484
|
+
return {
|
|
485
|
+
shown,
|
|
486
|
+
...(seed ? {
|
|
487
|
+
seed
|
|
488
|
+
} : {})
|
|
489
|
+
};
|
|
490
|
+
} catch {
|
|
491
|
+
return {
|
|
492
|
+
shown: false
|
|
493
|
+
};
|
|
494
|
+
}
|
|
441
495
|
};
|
|
442
496
|
const refresh = sessionID => {
|
|
443
497
|
const freshness = {
|
|
@@ -482,7 +536,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
482
536
|
}
|
|
483
537
|
if (!isAttachable(session)) continue;
|
|
484
538
|
deferred.splice(i, 1);
|
|
485
|
-
refreshBranch(session, true);
|
|
539
|
+
refreshBranch(session.id, true);
|
|
486
540
|
progressed = true;
|
|
487
541
|
}
|
|
488
542
|
}
|
|
@@ -509,9 +563,20 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
509
563
|
pendingBranches.clear();
|
|
510
564
|
failedMembers.clear();
|
|
511
565
|
tombstones.clear();
|
|
566
|
+
endedParts.clear();
|
|
512
567
|
setRemote(undefined);
|
|
513
568
|
solid.untrack(() => {
|
|
514
|
-
|
|
569
|
+
const turn = scanTurnState(sessionID);
|
|
570
|
+
ttftTurnShown = turn.shown;
|
|
571
|
+
if (turn.seed) {
|
|
572
|
+
ttftTurnShown = true;
|
|
573
|
+
setLiveTtft({
|
|
574
|
+
messageID: turn.seed.id,
|
|
575
|
+
createdAt: turn.seed.time.created,
|
|
576
|
+
now: Date.now()
|
|
577
|
+
});
|
|
578
|
+
ensureTimer();
|
|
579
|
+
}
|
|
515
580
|
refresh(sessionID);
|
|
516
581
|
});
|
|
517
582
|
});
|
|
@@ -533,11 +598,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
533
598
|
memberRequests.set(memberID, memberRequest);
|
|
534
599
|
void api.client.session.get({
|
|
535
600
|
sessionID: memberID
|
|
536
|
-
}
|
|
537
|
-
throwOnError: true
|
|
538
|
-
}).then(({
|
|
539
|
-
data
|
|
540
|
-
}) => {
|
|
601
|
+
}).then(data => {
|
|
541
602
|
if (!data) throw new Error("session unavailable");
|
|
542
603
|
return fetchContribution(api.client, data);
|
|
543
604
|
}).then(contribution => {
|
|
@@ -549,20 +610,25 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
549
610
|
failedMembers.add(memberID);
|
|
550
611
|
});
|
|
551
612
|
};
|
|
552
|
-
const refreshBranch = (
|
|
553
|
-
if (isNew) members = new Set([...members,
|
|
613
|
+
const refreshBranch = (branchID, isNew) => {
|
|
614
|
+
if (isNew) members = new Set([...members, branchID]);
|
|
554
615
|
const freshness = captureFreshness();
|
|
555
616
|
const branchRequest = ++nextAsyncRequest;
|
|
556
|
-
branchRequests.set(
|
|
557
|
-
void
|
|
617
|
+
branchRequests.set(branchID, branchRequest);
|
|
618
|
+
void api.client.session.get({
|
|
619
|
+
sessionID: branchID
|
|
620
|
+
}).then(root => {
|
|
621
|
+
if (!root) throw new Error("session unavailable");
|
|
622
|
+
return fetchBranch(api.client, root);
|
|
623
|
+
}).then(branch => {
|
|
558
624
|
setRemote(previous => {
|
|
559
|
-
if (!isCurrent(previous, freshness) || branchRequests.get(
|
|
625
|
+
if (!isCurrent(previous, freshness) || branchRequests.get(branchID) !== branchRequest || !previous.contributions) {
|
|
560
626
|
return previous;
|
|
561
627
|
}
|
|
562
628
|
const contributions = new Map(previous.contributions);
|
|
563
629
|
const changedIds = new Set();
|
|
564
630
|
for (const id of previous.contributions.keys()) {
|
|
565
|
-
if (belongsToBranch(id,
|
|
631
|
+
if (belongsToBranch(id, branchID, previous.contributions) && !branch.contributions.has(id) && !belongsToIncompleteBranch(id, branch.incompleteBranches, previous.contributions) && !hasNewerIncremental(id, freshness)) {
|
|
566
632
|
contributions.delete(id);
|
|
567
633
|
changedIds.add(id);
|
|
568
634
|
}
|
|
@@ -575,7 +641,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
575
641
|
}
|
|
576
642
|
const incompleteBranches = new Set(previous.incompleteBranches);
|
|
577
643
|
for (const id of incompleteBranches) {
|
|
578
|
-
if (belongsToBranch(id,
|
|
644
|
+
if (belongsToBranch(id, branchID, previous.contributions)) {
|
|
579
645
|
incompleteBranches.delete(id);
|
|
580
646
|
}
|
|
581
647
|
}
|
|
@@ -585,20 +651,12 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
585
651
|
return publishFamily(freshness.sessionID, contributions, incompleteBranches);
|
|
586
652
|
});
|
|
587
653
|
}).catch(() => {
|
|
588
|
-
if (isNew) members = new Set([...members].filter(id => id !==
|
|
654
|
+
if (isNew) members = new Set([...members].filter(id => id !== branchID));
|
|
589
655
|
});
|
|
590
656
|
};
|
|
591
657
|
const retryIncompleteBranches = () => {
|
|
592
|
-
for (const
|
|
593
|
-
|
|
594
|
-
sessionID
|
|
595
|
-
}, {
|
|
596
|
-
throwOnError: true
|
|
597
|
-
}).then(({
|
|
598
|
-
data
|
|
599
|
-
}) => {
|
|
600
|
-
if (data) refreshBranch(data, false);
|
|
601
|
-
}).catch(() => {});
|
|
658
|
+
for (const branchID of remote()?.incompleteBranches ?? []) {
|
|
659
|
+
refreshBranch(branchID, false);
|
|
602
660
|
}
|
|
603
661
|
for (const memberID of failedMembers) {
|
|
604
662
|
if (!members.has(memberID)) {
|
|
@@ -609,6 +667,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
609
667
|
}
|
|
610
668
|
};
|
|
611
669
|
const addBranch = session => {
|
|
670
|
+
if (session.parentID) parents.set(session.id, session.parentID);
|
|
612
671
|
if (members.has(session.id)) return;
|
|
613
672
|
if (!session.parentID) return;
|
|
614
673
|
if (!remote()?.contributions) {
|
|
@@ -617,13 +676,18 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
617
676
|
return;
|
|
618
677
|
}
|
|
619
678
|
if (!isAttachable(session)) return;
|
|
620
|
-
refreshBranch(session, true);
|
|
679
|
+
refreshBranch(session.id, true);
|
|
621
680
|
};
|
|
622
|
-
const offSessionCreated = api.
|
|
623
|
-
addBranch(
|
|
681
|
+
const offSessionCreated = api.data.on("session.created", event => {
|
|
682
|
+
addBranch({
|
|
683
|
+
id: event.data.sessionID,
|
|
684
|
+
...(event.data.parentID ? {
|
|
685
|
+
parentID: event.data.parentID
|
|
686
|
+
} : {})
|
|
687
|
+
});
|
|
624
688
|
});
|
|
625
|
-
const offSessionDeleted = api.
|
|
626
|
-
const deletedID = event.
|
|
689
|
+
const offSessionDeleted = api.data.on("session.deleted", event => {
|
|
690
|
+
const deletedID = event.data.sessionID;
|
|
627
691
|
const tracked = deletedID === sessionId() || members.has(deletedID) || pendingBranches.has(deletedID);
|
|
628
692
|
const pruned = new Set([deletedID]);
|
|
629
693
|
let expanded = true;
|
|
@@ -658,103 +722,109 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
658
722
|
refresh(sessionId());
|
|
659
723
|
}
|
|
660
724
|
});
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
725
|
+
const offUsageUpdated = api.data.on("session.usage.updated", event => {
|
|
726
|
+
const {
|
|
727
|
+
sessionID,
|
|
728
|
+
cost,
|
|
729
|
+
tokens
|
|
730
|
+
} = event.data;
|
|
731
|
+
if (pendingBranches.has(sessionID)) return;
|
|
732
|
+
if (!members.has(sessionID)) return;
|
|
733
|
+
const freshness = captureFreshness();
|
|
734
|
+
setRemote(previous => {
|
|
735
|
+
if (!isCurrent(previous, freshness) || !previous.contributions) return previous;
|
|
736
|
+
const prior = previous.contributions.get(sessionID);
|
|
737
|
+
const parentID = prior?.parentID ?? parents.get(sessionID);
|
|
738
|
+
const contributions = new Map(previous.contributions);
|
|
739
|
+
contributions.set(sessionID, {
|
|
740
|
+
...(prior ?? {
|
|
741
|
+
metrics: emptyMetrics()
|
|
742
|
+
}),
|
|
743
|
+
totals: totalsFromUsage(tokens, cost),
|
|
744
|
+
...(parentID ? {
|
|
745
|
+
parentID
|
|
746
|
+
} : {})
|
|
747
|
+
});
|
|
748
|
+
markRevised([sessionID]);
|
|
749
|
+
failedMembers.delete(sessionID);
|
|
750
|
+
return publishFamily(freshness.sessionID, contributions, previous.incompleteBranches);
|
|
751
|
+
});
|
|
752
|
+
retryIncompleteBranches();
|
|
677
753
|
});
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
754
|
+
const offStepStarted = api.data.on("session.step.started", event => {
|
|
755
|
+
const {
|
|
756
|
+
sessionID,
|
|
757
|
+
assistantMessageID,
|
|
758
|
+
started
|
|
759
|
+
} = event.data;
|
|
760
|
+
if (sessionID !== sessionId()) return;
|
|
761
|
+
if (ttftTurnShown) return;
|
|
762
|
+
ttftTurnShown = true;
|
|
763
|
+
setLiveTtft({
|
|
764
|
+
messageID: assistantMessageID,
|
|
765
|
+
createdAt: started,
|
|
766
|
+
now: Date.now()
|
|
767
|
+
});
|
|
768
|
+
ensureTimer();
|
|
681
769
|
});
|
|
682
|
-
const
|
|
683
|
-
if (event.
|
|
684
|
-
|
|
770
|
+
const offExecutionStarted = api.data.on("session.execution.started", event => {
|
|
771
|
+
if (event.data.sessionID !== sessionId()) return;
|
|
772
|
+
ttftTurnShown = false;
|
|
685
773
|
});
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
retryIncompleteBranches();
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
addBranch(event.properties.info);
|
|
774
|
+
const offStepStreamed = api.data.on("session.step.streamed", event => {
|
|
775
|
+
const {
|
|
776
|
+
sessionID,
|
|
777
|
+
assistantMessageID
|
|
778
|
+
} = event.data;
|
|
779
|
+
if (sessionID !== sessionId()) return;
|
|
780
|
+
if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
697
781
|
});
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (message.time.completed !== undefined) {
|
|
703
|
-
refreshMember(event.properties.sessionID);
|
|
704
|
-
retryIncompleteBranches();
|
|
705
|
-
}
|
|
706
|
-
return;
|
|
782
|
+
const onStepSettled = (sessionID, assistantMessageID) => {
|
|
783
|
+
if (sessionID === sessionId()) {
|
|
784
|
+
if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
785
|
+
ttftTurnShown = true;
|
|
707
786
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const now = Date.now();
|
|
717
|
-
setLiveTtft({
|
|
718
|
-
messageID: message.id,
|
|
719
|
-
createdAt: message.time.created,
|
|
720
|
-
now
|
|
721
|
-
});
|
|
722
|
-
ensureTimer();
|
|
787
|
+
refreshMember(sessionID);
|
|
788
|
+
retryIncompleteBranches();
|
|
789
|
+
};
|
|
790
|
+
const offStepEnded = api.data.on("session.step.ended", event => {
|
|
791
|
+
onStepSettled(event.data.sessionID, event.data.assistantMessageID);
|
|
792
|
+
});
|
|
793
|
+
const offStepFailed = api.data.on("session.step.failed", event => {
|
|
794
|
+
onStepSettled(event.data.sessionID, event.data.assistantMessageID);
|
|
723
795
|
});
|
|
724
|
-
const
|
|
796
|
+
const onStreamDelta = (kind, stream) => {
|
|
725
797
|
const {
|
|
726
798
|
sessionID,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
field,
|
|
799
|
+
assistantMessageID,
|
|
800
|
+
ordinal,
|
|
730
801
|
delta
|
|
731
|
-
} =
|
|
732
|
-
if (sessionID !== sessionId()
|
|
733
|
-
|
|
802
|
+
} = stream;
|
|
803
|
+
if (sessionID !== sessionId()) return;
|
|
804
|
+
const partKey = `${kind}:${assistantMessageID}:${ordinal}`;
|
|
805
|
+
if (endedParts.has(partKey)) return;
|
|
734
806
|
let message;
|
|
735
807
|
try {
|
|
736
|
-
|
|
737
|
-
message = api.state.session.messages(sessionID).find(candidate => candidate.id === messageID);
|
|
808
|
+
message = api.data.session.message.list(sessionID).find(candidate => candidate.id === assistantMessageID);
|
|
738
809
|
} catch {
|
|
739
810
|
return;
|
|
740
811
|
}
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
}
|
|
812
|
+
if (message?.type !== "assistant" || message.time.completed !== undefined) return;
|
|
813
|
+
const streaming = message;
|
|
744
814
|
const now = Date.now();
|
|
745
815
|
const chars = codePoints(delta);
|
|
746
816
|
setLiveSpeed(current => {
|
|
747
|
-
if (current?.
|
|
817
|
+
if (current?.partKey === partKey) {
|
|
748
818
|
return {
|
|
749
819
|
...current,
|
|
750
820
|
chars: current.chars + chars
|
|
751
821
|
};
|
|
752
822
|
}
|
|
753
|
-
const calibration = remote()?.metrics?.calibrations.get(modelKey(
|
|
823
|
+
const calibration = remote()?.metrics?.calibrations.get(modelKey(streaming));
|
|
754
824
|
const charsPerToken = (400 + (calibration?.chars ?? 0)) / (100 + (calibration?.tokens ?? 0));
|
|
755
825
|
return {
|
|
756
|
-
messageID,
|
|
757
|
-
|
|
826
|
+
messageID: assistantMessageID,
|
|
827
|
+
partKey,
|
|
758
828
|
startedAt: now,
|
|
759
829
|
now,
|
|
760
830
|
chars,
|
|
@@ -763,32 +833,64 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
763
833
|
hasTicked: false
|
|
764
834
|
};
|
|
765
835
|
});
|
|
766
|
-
if (liveTtft()?.messageID ===
|
|
836
|
+
if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
767
837
|
ensureTimer();
|
|
838
|
+
};
|
|
839
|
+
const offTextDelta = api.data.on("session.text.delta", event => {
|
|
840
|
+
onStreamDelta("text", event.data);
|
|
841
|
+
});
|
|
842
|
+
const offReasoningDelta = api.data.on("session.reasoning.delta", event => {
|
|
843
|
+
onStreamDelta("reasoning", event.data);
|
|
844
|
+
});
|
|
845
|
+
const onStreamEnded = (kind, end) => {
|
|
846
|
+
const {
|
|
847
|
+
sessionID,
|
|
848
|
+
assistantMessageID,
|
|
849
|
+
ordinal
|
|
850
|
+
} = end;
|
|
851
|
+
if (sessionID !== sessionId()) return;
|
|
852
|
+
const partKey = `${kind}:${assistantMessageID}:${ordinal}`;
|
|
853
|
+
endedParts.add(partKey);
|
|
854
|
+
if (liveSpeed()?.partKey === partKey) clearLiveSpeed();
|
|
855
|
+
};
|
|
856
|
+
const offTextEnded = api.data.on("session.text.ended", event => {
|
|
857
|
+
onStreamEnded("text", event.data);
|
|
858
|
+
});
|
|
859
|
+
const offReasoningEnded = api.data.on("session.reasoning.ended", event => {
|
|
860
|
+
onStreamEnded("reasoning", event.data);
|
|
861
|
+
});
|
|
862
|
+
const offContentUpdated = api.data.on("session.revert.committed", event => {
|
|
863
|
+
if (event.data.sessionID === sessionId()) clearProvisional();
|
|
864
|
+
if (members.has(event.data.sessionID)) refresh(sessionId());
|
|
768
865
|
});
|
|
769
|
-
const offServerConnected = api.
|
|
866
|
+
const offServerConnected = api.data.on("server.connected", () => {
|
|
770
867
|
clearProvisional();
|
|
771
868
|
refresh(sessionId());
|
|
772
869
|
});
|
|
773
|
-
const
|
|
774
|
-
if (!event.
|
|
870
|
+
const offExecutionFailed = api.data.on("session.execution.failed", event => {
|
|
871
|
+
if (!event.data.sessionID || event.data.sessionID === sessionId()) clearProvisional();
|
|
775
872
|
});
|
|
776
|
-
const offSessionIdle = api.
|
|
777
|
-
if (event.
|
|
873
|
+
const offSessionIdle = api.data.on("session.idle", event => {
|
|
874
|
+
if (event.data.sessionID === sessionId()) clearProvisional();
|
|
778
875
|
});
|
|
779
876
|
solid.onCleanup(() => {
|
|
780
877
|
request++;
|
|
781
878
|
clearProvisional();
|
|
782
879
|
offSessionCreated();
|
|
783
880
|
offSessionDeleted();
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
881
|
+
offUsageUpdated();
|
|
882
|
+
offStepStarted();
|
|
883
|
+
offExecutionStarted();
|
|
884
|
+
offStepStreamed();
|
|
885
|
+
offStepEnded();
|
|
886
|
+
offStepFailed();
|
|
887
|
+
offTextDelta();
|
|
888
|
+
offReasoningDelta();
|
|
889
|
+
offTextEnded();
|
|
890
|
+
offReasoningEnded();
|
|
891
|
+
offContentUpdated();
|
|
790
892
|
offServerConnected();
|
|
791
|
-
|
|
893
|
+
offExecutionFailed();
|
|
792
894
|
offSessionIdle();
|
|
793
895
|
});
|
|
794
896
|
const snapshot = solid.createMemo(() => {
|
|
@@ -805,7 +907,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
805
907
|
hasDescendants: false
|
|
806
908
|
};
|
|
807
909
|
}
|
|
808
|
-
const totals = loaded?.sessionID === sessionID && loaded.totals ? loaded.totals : totalsFromSession(api.
|
|
910
|
+
const totals = loaded?.sessionID === sessionID && loaded.totals ? loaded.totals : totalsFromSession(api.data.session.get(sessionID));
|
|
809
911
|
const hasDescendants = loaded?.sessionID === sessionID ? loaded.hasDescendants : false;
|
|
810
912
|
if (!totals) {
|
|
811
913
|
const speed = liveSpeed();
|
package/dist/usage-panel.js
CHANGED
|
@@ -10,8 +10,9 @@ import { createElement as _$createElement } from "@opentui/solid";
|
|
|
10
10
|
* Usage of the current session family: token totals, cache rate, completed
|
|
11
11
|
* step count, generation speed/TTFT, and provisional diagnostics for the
|
|
12
12
|
* visible stream.
|
|
13
|
-
* Family totals include all subagent descendants; when
|
|
14
|
-
*
|
|
13
|
+
* Family totals include all subagent descendants; when the current session
|
|
14
|
+
* itself has descendant sessions the section title becomes
|
|
15
|
+
* "Token Usage (including subagents)", otherwise it stays "Token Usage".
|
|
15
16
|
*
|
|
16
17
|
* This file is only the View plus slot registration: it computes no numbers
|
|
17
18
|
* and formats nothing — all logic lives in ./usage-model.ts (the tested seam).
|
|
@@ -21,6 +22,7 @@ import { createElement as _$createElement } from "@opentui/solid";
|
|
|
21
22
|
*/
|
|
22
23
|
/** @jsxImportSource @opentui/solid */
|
|
23
24
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show, untrack } from "solid-js";
|
|
25
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
24
26
|
import { USAGE_SECTION_TITLE, USAGE_SECTION_TITLE_WITH_SUBAGENTS, USAGE_STATUS_TEXT, createUsageModel } from "./usage-model.js";
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -37,9 +39,9 @@ const solid = {
|
|
|
37
39
|
untrack
|
|
38
40
|
};
|
|
39
41
|
function Section(props) {
|
|
40
|
-
const theme = () => props.
|
|
42
|
+
const theme = () => props.context.theme;
|
|
41
43
|
const [collapsed, setCollapsed] = createSignal(false);
|
|
42
|
-
const model = createUsageModel(props.
|
|
44
|
+
const model = createUsageModel(props.context, () => props.sessionID, solid);
|
|
43
45
|
return (() => {
|
|
44
46
|
var _el$ = _$createElement("box"),
|
|
45
47
|
_el$2 = _$createElement("box"),
|
|
@@ -67,7 +69,7 @@ function Section(props) {
|
|
|
67
69
|
get children() {
|
|
68
70
|
var _el$6 = _$createElement("text");
|
|
69
71
|
_$insert(_el$6, () => USAGE_STATUS_TEXT[model.status()]);
|
|
70
|
-
_$effect(_$p => _$setProp(_el$6, "fg", theme().
|
|
72
|
+
_$effect(_$p => _$setProp(_el$6, "fg", theme().text.muted, _$p));
|
|
71
73
|
return _el$6;
|
|
72
74
|
}
|
|
73
75
|
}), _$createComponent(For, {
|
|
@@ -85,8 +87,8 @@ function Section(props) {
|
|
|
85
87
|
_$insert(_el$8, () => row.label);
|
|
86
88
|
_$insert(_el$9, () => row.value);
|
|
87
89
|
_$effect(_p$ => {
|
|
88
|
-
var _v$3 = theme().
|
|
89
|
-
_v$4 = theme().text;
|
|
90
|
+
var _v$3 = theme().text.muted,
|
|
91
|
+
_v$4 = theme().text.base;
|
|
90
92
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$8, "fg", _v$3, _p$.e));
|
|
91
93
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$9, "fg", _v$4, _p$.t));
|
|
92
94
|
return _p$;
|
|
@@ -100,8 +102,8 @@ function Section(props) {
|
|
|
100
102
|
}
|
|
101
103
|
}), null);
|
|
102
104
|
_$effect(_p$ => {
|
|
103
|
-
var _v$ = theme().text,
|
|
104
|
-
_v$2 = theme().text;
|
|
105
|
+
var _v$ = theme().text.base,
|
|
106
|
+
_v$2 = theme().text.base;
|
|
105
107
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
|
|
106
108
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
|
|
107
109
|
return _p$;
|
|
@@ -112,25 +114,21 @@ function Section(props) {
|
|
|
112
114
|
return _el$;
|
|
113
115
|
})();
|
|
114
116
|
}
|
|
115
|
-
|
|
116
|
-
// Order 150: internal sidebar sections sit at 100/200/300/400/500, so this
|
|
117
|
-
// lands right after the first block without moving any existing section.
|
|
118
|
-
api.slots.register({
|
|
119
|
-
order: 150,
|
|
120
|
-
slots: {
|
|
121
|
-
sidebar_content(_ctx, props) {
|
|
122
|
-
return _$createComponent(Section, {
|
|
123
|
-
api: api,
|
|
124
|
-
get session_id() {
|
|
125
|
-
return props.session_id;
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
};
|
|
132
|
-
const plugin = {
|
|
117
|
+
export default Plugin.define({
|
|
133
118
|
id: "supercode.token-usage",
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
119
|
+
setup(context) {
|
|
120
|
+
// `after`, not `append`: a replace takeover of this path (e.g.
|
|
121
|
+
// context-progress-bar's hideMcp) suppresses every append/prepend claim
|
|
122
|
+
// on it, while before/after claims render as siblings around the
|
|
123
|
+
// boundary. Content still lands below the built-in sidebar sections.
|
|
124
|
+
context.ui.slot({
|
|
125
|
+
after: "sidebar.content",
|
|
126
|
+
render: props => _$createComponent(Section, {
|
|
127
|
+
context: context,
|
|
128
|
+
get sessionID() {
|
|
129
|
+
return props.sessionID;
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@glaicer/supercode-token-usage-panel",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"description": "OpenCode TUI sidebar section: cumulative token usage and cost of the current session family.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,21 +37,26 @@
|
|
|
37
37
|
"typecheck": "tsc --noEmit"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@opencode
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
40
|
+
"@opencode/plugin": ">=2.0.16"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@opentui/core": ">=0.5.10",
|
|
44
|
+
"@opentui/solid": ">=0.5.10",
|
|
45
|
+
"solid-js": ">=1.9.0"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@babel/core": "7.28.0",
|
|
47
49
|
"@babel/preset-typescript": "7.27.1",
|
|
48
|
-
"@opencode
|
|
50
|
+
"@opencode/client": "2.0.16",
|
|
51
|
+
"@opentui/core": "0.5.12",
|
|
52
|
+
"@opentui/solid": "0.5.12",
|
|
49
53
|
"@types/node": "24.13.3",
|
|
50
54
|
"babel-preset-solid": "1.9.12",
|
|
55
|
+
"solid-js": "1.9.12",
|
|
51
56
|
"typescript": "5.8.2"
|
|
52
57
|
},
|
|
53
58
|
"engines": {
|
|
54
59
|
"node": ">=24",
|
|
55
|
-
"opencode": ">=
|
|
60
|
+
"opencode": ">=2.0.0"
|
|
56
61
|
}
|
|
57
62
|
}
|