@glaicer/supercode-token-usage-panel 0.1.4 → 1.0.1
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 +307 -209
- package/dist/usage-panel.js +25 -28
- 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). `time.streamed`
|
|
10
|
+
* marks the end of the provider stream; first output comes from content-start
|
|
11
|
+
* events or the timestamp of the first reasoning/tool content item.
|
|
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,41 @@ function addTotals(target, source) {
|
|
|
158
173
|
target.cacheWrite += source.cacheWrite;
|
|
159
174
|
target.cost += source.cost;
|
|
160
175
|
}
|
|
161
|
-
|
|
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
|
+
}
|
|
183
|
+
function completedMetrics(messages, firstOutputs) {
|
|
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
|
-
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (part.type === "tool") {
|
|
181
|
-
stepHasTool = true;
|
|
191
|
+
for (const item of info.content) {
|
|
192
|
+
if (item.type === "text" || item.type === "reasoning") {
|
|
193
|
+
stepChars += codePoints(item.text);
|
|
182
194
|
continue;
|
|
183
195
|
}
|
|
184
|
-
if (
|
|
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
|
-
|
|
198
|
+
if (info.tokens && !stepHasTool) {
|
|
199
|
+
addCalibration(result.calibrations, modelKey(info), stepChars, info.tokens.output + info.tokens.reasoning);
|
|
200
|
+
}
|
|
201
|
+
const streamed = info.time.streamed;
|
|
202
|
+
if (!positive(streamed ?? 0)) continue;
|
|
203
|
+
const head = info.content[0];
|
|
204
|
+
const first = firstOutputs.get(info.id) ?? (head && head.type !== "text" ? head.time?.created : undefined);
|
|
205
|
+
if (first === undefined || first < info.time.created || first > streamed) continue;
|
|
206
|
+
const ttft = first - info.time.created;
|
|
199
207
|
result.ttftMs += ttft;
|
|
200
208
|
result.ttftCount++;
|
|
201
|
-
const generated =
|
|
202
|
-
|
|
203
|
-
return sum + part.tokens.output + part.tokens.reasoning;
|
|
204
|
-
}, 0);
|
|
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;
|
|
208
|
-
return positive(duration) ? sum + duration : sum;
|
|
209
|
-
}, 0);
|
|
210
|
-
const decode = message.time.completed - message.time.created - ttft - tools;
|
|
209
|
+
const generated = info.tokens ? info.tokens.output + info.tokens.reasoning : 0;
|
|
210
|
+
const decode = streamed - first;
|
|
211
211
|
if (!positive(generated) || !positive(decode)) continue;
|
|
212
212
|
result.generated += generated;
|
|
213
213
|
result.decodeMs += decode;
|
|
@@ -259,15 +259,25 @@ function aggregateContributions(contributions, incompleteBranches = new Set(), v
|
|
|
259
259
|
incompleteBranches
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
|
-
async function
|
|
262
|
+
async function listAllMessages(client, sessionID) {
|
|
263
|
+
const collected = [];
|
|
264
|
+
let cursor;
|
|
265
|
+
do {
|
|
266
|
+
const page = await client.message.list({
|
|
267
|
+
sessionID,
|
|
268
|
+
...(cursor ? {
|
|
269
|
+
cursor
|
|
270
|
+
} : {})
|
|
271
|
+
});
|
|
272
|
+
collected.push(...page.data);
|
|
273
|
+
cursor = page.cursor?.next ?? undefined;
|
|
274
|
+
} while (cursor);
|
|
275
|
+
return collected.sort((a, b) => (a.time.created ?? 0) - (b.time.created ?? 0));
|
|
276
|
+
}
|
|
277
|
+
async function fetchContribution(client, session, firstOutputs) {
|
|
263
278
|
let metrics = emptyMetrics();
|
|
264
279
|
try {
|
|
265
|
-
|
|
266
|
-
sessionID: session.id
|
|
267
|
-
}, {
|
|
268
|
-
throwOnError: true
|
|
269
|
-
})).data;
|
|
270
|
-
metrics = completedMetrics(messages);
|
|
280
|
+
metrics = completedMetrics(await listAllMessages(client, session.id), firstOutputs);
|
|
271
281
|
} catch {
|
|
272
282
|
// Totals stay usable when diagnostic history cannot be read.
|
|
273
283
|
}
|
|
@@ -279,7 +289,7 @@ async function fetchContribution(client, session) {
|
|
|
279
289
|
} : {})
|
|
280
290
|
};
|
|
281
291
|
}
|
|
282
|
-
async function fetchBranch(client, root) {
|
|
292
|
+
async function fetchBranch(client, root, firstOutputs) {
|
|
283
293
|
const contributions = new Map();
|
|
284
294
|
const incompleteBranches = new Set();
|
|
285
295
|
const queue = [root];
|
|
@@ -288,12 +298,10 @@ async function fetchBranch(client, root) {
|
|
|
288
298
|
const session = queue.shift();
|
|
289
299
|
if (visited.has(session.id)) continue;
|
|
290
300
|
visited.add(session.id);
|
|
291
|
-
contributions.set(session.id, await fetchContribution(client, session));
|
|
301
|
+
contributions.set(session.id, await fetchContribution(client, session, firstOutputs));
|
|
292
302
|
try {
|
|
293
|
-
const children = (await client.session.
|
|
294
|
-
|
|
295
|
-
}, {
|
|
296
|
-
throwOnError: true
|
|
303
|
+
const children = (await client.session.list({
|
|
304
|
+
parentID: session.id
|
|
297
305
|
})).data;
|
|
298
306
|
for (const child of children) {
|
|
299
307
|
if (child && !visited.has(child.id)) queue.push(child);
|
|
@@ -322,29 +330,25 @@ function belongsToBranch(sessionID, rootID, contributions) {
|
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
/**
|
|
325
|
-
* Totals cover the
|
|
326
|
-
* The walk counts each session once and keeps
|
|
327
|
-
* branch fails to resolve.
|
|
333
|
+
* Totals cover the whole family resolved from the root: the root session plus
|
|
334
|
+
* all descendants (subagents). The walk counts each session once and keeps
|
|
335
|
+
* resolved aggregates when a branch fails to resolve.
|
|
328
336
|
*/
|
|
329
|
-
async function fetchFamily(client, sessionID) {
|
|
330
|
-
let root =
|
|
337
|
+
async function fetchFamily(client, sessionID, firstOutputs) {
|
|
338
|
+
let root = await client.session.get({
|
|
331
339
|
sessionID
|
|
332
|
-
}
|
|
333
|
-
throwOnError: true
|
|
334
|
-
})).data;
|
|
340
|
+
});
|
|
335
341
|
if (!root) throw new Error("session unavailable");
|
|
336
342
|
const ancestors = new Set([root.id]);
|
|
337
343
|
while (root.parentID && !ancestors.has(root.parentID)) {
|
|
338
344
|
ancestors.add(root.parentID);
|
|
339
|
-
const parent =
|
|
345
|
+
const parent = await client.session.get({
|
|
340
346
|
sessionID: root.parentID
|
|
341
|
-
}
|
|
342
|
-
throwOnError: true
|
|
343
|
-
})).data;
|
|
347
|
+
});
|
|
344
348
|
if (!parent) throw new Error("parent session unavailable");
|
|
345
349
|
root = parent;
|
|
346
350
|
}
|
|
347
|
-
return fetchBranch(client, root);
|
|
351
|
+
return fetchBranch(client, root, firstOutputs);
|
|
348
352
|
}
|
|
349
353
|
|
|
350
354
|
/**
|
|
@@ -359,9 +363,9 @@ async function fetchFamily(client, sessionID) {
|
|
|
359
363
|
*/
|
|
360
364
|
|
|
361
365
|
/**
|
|
362
|
-
* Usage Model over OpenCode's session aggregate. Totals cover the
|
|
363
|
-
* session plus all
|
|
364
|
-
* slower responses from overwriting newer ones.
|
|
366
|
+
* Usage Model over OpenCode's session aggregate. Totals cover the whole family
|
|
367
|
+
* resolved from the root (the session plus all subagent descendants); request
|
|
368
|
+
* sequencing keeps slower responses from overwriting newer ones.
|
|
365
369
|
*/
|
|
366
370
|
export function createUsageModel(api, sessionId, solid) {
|
|
367
371
|
const [remote, setRemote] = solid.createSignal();
|
|
@@ -375,10 +379,15 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
375
379
|
const contributionRevisions = new Map();
|
|
376
380
|
const pendingBranches = new Map();
|
|
377
381
|
const failedMembers = new Set();
|
|
382
|
+
/** Parent links announced by events, for contributions created before any fetch lands. */
|
|
383
|
+
const parents = new Map();
|
|
378
384
|
/** Deleted session ids; a full refresh must never resurrect their totals. */
|
|
379
385
|
const tombstones = new Set();
|
|
386
|
+
/** Content items whose stream already ended; stale deltas must not restart them. */
|
|
387
|
+
const endedParts = new Set();
|
|
388
|
+
const firstOutputs = new Map();
|
|
380
389
|
let timer;
|
|
381
|
-
let
|
|
390
|
+
let ttftTurnShown = false;
|
|
382
391
|
let members = new Set();
|
|
383
392
|
const isAttachable = session => !!session.parentID && members.has(session.parentID) && !members.has(session.id);
|
|
384
393
|
const captureFreshness = () => ({
|
|
@@ -396,6 +405,9 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
396
405
|
const publishFamily = (sessionID, contributions, incompleteBranches) => {
|
|
397
406
|
const aggregate = aggregateContributions(contributions, incompleteBranches, sessionID);
|
|
398
407
|
members = aggregate.members;
|
|
408
|
+
for (const [id, contribution] of contributions) {
|
|
409
|
+
if (contribution.parentID) parents.set(id, contribution.parentID);
|
|
410
|
+
}
|
|
399
411
|
return {
|
|
400
412
|
sessionID,
|
|
401
413
|
totals: aggregate.totals,
|
|
@@ -442,16 +454,37 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
442
454
|
if (timer) clearInterval(timer);
|
|
443
455
|
timer = undefined;
|
|
444
456
|
};
|
|
445
|
-
|
|
446
|
-
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Live TTFT belongs to the first assistant step of a turn only; later steps
|
|
460
|
+
* follow tool calls and would report tool latency as "time to first token".
|
|
461
|
+
* A turn is the run of assistant messages after the last non-assistant
|
|
462
|
+
* message; it is already spoken for when any of its steps finished. When the
|
|
463
|
+
* panel mounts mid-first-step the seeded measurement keeps the elapsed time
|
|
464
|
+
* visible from the step's start.
|
|
465
|
+
*/
|
|
466
|
+
const scanTurnState = sessionID => {
|
|
447
467
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
468
|
+
const messages = api.data.session.message.list(sessionID);
|
|
469
|
+
const run = [];
|
|
470
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
471
|
+
const message = messages[index];
|
|
472
|
+
if (!message || message.type !== "assistant") break;
|
|
473
|
+
run.unshift(message);
|
|
452
474
|
}
|
|
453
|
-
|
|
454
|
-
|
|
475
|
+
const shown = run.some(finishedStep);
|
|
476
|
+
const seed = !shown ? run[0] : undefined;
|
|
477
|
+
return {
|
|
478
|
+
shown,
|
|
479
|
+
...(seed ? {
|
|
480
|
+
seed
|
|
481
|
+
} : {})
|
|
482
|
+
};
|
|
483
|
+
} catch {
|
|
484
|
+
return {
|
|
485
|
+
shown: false
|
|
486
|
+
};
|
|
487
|
+
}
|
|
455
488
|
};
|
|
456
489
|
const refresh = sessionID => {
|
|
457
490
|
const freshness = {
|
|
@@ -459,7 +492,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
459
492
|
sessionID,
|
|
460
493
|
startRevision: appliedRevision
|
|
461
494
|
};
|
|
462
|
-
void fetchFamily(api.client, sessionID).then(family => {
|
|
495
|
+
void fetchFamily(api.client, sessionID, firstOutputs).then(family => {
|
|
463
496
|
if (!isFresh(freshness)) return;
|
|
464
497
|
setRemote(previous => {
|
|
465
498
|
const contributions = new Map(family.contributions);
|
|
@@ -496,7 +529,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
496
529
|
}
|
|
497
530
|
if (!isAttachable(session)) continue;
|
|
498
531
|
deferred.splice(i, 1);
|
|
499
|
-
refreshBranch(session, true);
|
|
532
|
+
refreshBranch(session.id, true);
|
|
500
533
|
progressed = true;
|
|
501
534
|
}
|
|
502
535
|
}
|
|
@@ -523,9 +556,21 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
523
556
|
pendingBranches.clear();
|
|
524
557
|
failedMembers.clear();
|
|
525
558
|
tombstones.clear();
|
|
559
|
+
endedParts.clear();
|
|
560
|
+
firstOutputs.clear();
|
|
526
561
|
setRemote(undefined);
|
|
527
562
|
solid.untrack(() => {
|
|
528
|
-
|
|
563
|
+
const turn = scanTurnState(sessionID);
|
|
564
|
+
ttftTurnShown = turn.shown;
|
|
565
|
+
if (turn.seed) {
|
|
566
|
+
ttftTurnShown = true;
|
|
567
|
+
setLiveTtft({
|
|
568
|
+
messageID: turn.seed.id,
|
|
569
|
+
createdAt: turn.seed.time.created,
|
|
570
|
+
now: Date.now()
|
|
571
|
+
});
|
|
572
|
+
ensureTimer();
|
|
573
|
+
}
|
|
529
574
|
refresh(sessionID);
|
|
530
575
|
});
|
|
531
576
|
});
|
|
@@ -547,13 +592,9 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
547
592
|
memberRequests.set(memberID, memberRequest);
|
|
548
593
|
void api.client.session.get({
|
|
549
594
|
sessionID: memberID
|
|
550
|
-
}
|
|
551
|
-
throwOnError: true
|
|
552
|
-
}).then(({
|
|
553
|
-
data
|
|
554
|
-
}) => {
|
|
595
|
+
}).then(data => {
|
|
555
596
|
if (!data) throw new Error("session unavailable");
|
|
556
|
-
return fetchContribution(api.client, data);
|
|
597
|
+
return fetchContribution(api.client, data, firstOutputs);
|
|
557
598
|
}).then(contribution => {
|
|
558
599
|
if (memberRequests.get(memberID) !== memberRequest) return;
|
|
559
600
|
mergeContributions(new Map([[memberID, contribution]]), freshness);
|
|
@@ -563,20 +604,25 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
563
604
|
failedMembers.add(memberID);
|
|
564
605
|
});
|
|
565
606
|
};
|
|
566
|
-
const refreshBranch = (
|
|
567
|
-
if (isNew) members = new Set([...members,
|
|
607
|
+
const refreshBranch = (branchID, isNew) => {
|
|
608
|
+
if (isNew) members = new Set([...members, branchID]);
|
|
568
609
|
const freshness = captureFreshness();
|
|
569
610
|
const branchRequest = ++nextAsyncRequest;
|
|
570
|
-
branchRequests.set(
|
|
571
|
-
void
|
|
611
|
+
branchRequests.set(branchID, branchRequest);
|
|
612
|
+
void api.client.session.get({
|
|
613
|
+
sessionID: branchID
|
|
614
|
+
}).then(root => {
|
|
615
|
+
if (!root) throw new Error("session unavailable");
|
|
616
|
+
return fetchBranch(api.client, root, firstOutputs);
|
|
617
|
+
}).then(branch => {
|
|
572
618
|
setRemote(previous => {
|
|
573
|
-
if (!isCurrent(previous, freshness) || branchRequests.get(
|
|
619
|
+
if (!isCurrent(previous, freshness) || branchRequests.get(branchID) !== branchRequest || !previous.contributions) {
|
|
574
620
|
return previous;
|
|
575
621
|
}
|
|
576
622
|
const contributions = new Map(previous.contributions);
|
|
577
623
|
const changedIds = new Set();
|
|
578
624
|
for (const id of previous.contributions.keys()) {
|
|
579
|
-
if (belongsToBranch(id,
|
|
625
|
+
if (belongsToBranch(id, branchID, previous.contributions) && !branch.contributions.has(id) && !belongsToIncompleteBranch(id, branch.incompleteBranches, previous.contributions) && !hasNewerIncremental(id, freshness)) {
|
|
580
626
|
contributions.delete(id);
|
|
581
627
|
changedIds.add(id);
|
|
582
628
|
}
|
|
@@ -589,7 +635,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
589
635
|
}
|
|
590
636
|
const incompleteBranches = new Set(previous.incompleteBranches);
|
|
591
637
|
for (const id of incompleteBranches) {
|
|
592
|
-
if (belongsToBranch(id,
|
|
638
|
+
if (belongsToBranch(id, branchID, previous.contributions)) {
|
|
593
639
|
incompleteBranches.delete(id);
|
|
594
640
|
}
|
|
595
641
|
}
|
|
@@ -599,20 +645,12 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
599
645
|
return publishFamily(freshness.sessionID, contributions, incompleteBranches);
|
|
600
646
|
});
|
|
601
647
|
}).catch(() => {
|
|
602
|
-
if (isNew) members = new Set([...members].filter(id => id !==
|
|
648
|
+
if (isNew) members = new Set([...members].filter(id => id !== branchID));
|
|
603
649
|
});
|
|
604
650
|
};
|
|
605
651
|
const retryIncompleteBranches = () => {
|
|
606
|
-
for (const
|
|
607
|
-
|
|
608
|
-
sessionID
|
|
609
|
-
}, {
|
|
610
|
-
throwOnError: true
|
|
611
|
-
}).then(({
|
|
612
|
-
data
|
|
613
|
-
}) => {
|
|
614
|
-
if (data) refreshBranch(data, false);
|
|
615
|
-
}).catch(() => {});
|
|
652
|
+
for (const branchID of remote()?.incompleteBranches ?? []) {
|
|
653
|
+
refreshBranch(branchID, false);
|
|
616
654
|
}
|
|
617
655
|
for (const memberID of failedMembers) {
|
|
618
656
|
if (!members.has(memberID)) {
|
|
@@ -623,6 +661,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
623
661
|
}
|
|
624
662
|
};
|
|
625
663
|
const addBranch = session => {
|
|
664
|
+
if (session.parentID) parents.set(session.id, session.parentID);
|
|
626
665
|
if (members.has(session.id)) return;
|
|
627
666
|
if (!session.parentID) return;
|
|
628
667
|
if (!remote()?.contributions) {
|
|
@@ -631,13 +670,18 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
631
670
|
return;
|
|
632
671
|
}
|
|
633
672
|
if (!isAttachable(session)) return;
|
|
634
|
-
refreshBranch(session, true);
|
|
673
|
+
refreshBranch(session.id, true);
|
|
635
674
|
};
|
|
636
|
-
const offSessionCreated = api.
|
|
637
|
-
addBranch(
|
|
675
|
+
const offSessionCreated = api.data.on("session.created", event => {
|
|
676
|
+
addBranch({
|
|
677
|
+
id: event.data.sessionID,
|
|
678
|
+
...(event.data.parentID ? {
|
|
679
|
+
parentID: event.data.parentID
|
|
680
|
+
} : {})
|
|
681
|
+
});
|
|
638
682
|
});
|
|
639
|
-
const offSessionDeleted = api.
|
|
640
|
-
const deletedID = event.
|
|
683
|
+
const offSessionDeleted = api.data.on("session.deleted", event => {
|
|
684
|
+
const deletedID = event.data.sessionID;
|
|
641
685
|
const tracked = deletedID === sessionId() || members.has(deletedID) || pendingBranches.has(deletedID);
|
|
642
686
|
const pruned = new Set([deletedID]);
|
|
643
687
|
let expanded = true;
|
|
@@ -672,103 +716,122 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
672
716
|
refresh(sessionId());
|
|
673
717
|
}
|
|
674
718
|
});
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
719
|
+
const offUsageUpdated = api.data.on("session.usage.updated", event => {
|
|
720
|
+
const {
|
|
721
|
+
sessionID,
|
|
722
|
+
cost,
|
|
723
|
+
tokens
|
|
724
|
+
} = event.data;
|
|
725
|
+
if (pendingBranches.has(sessionID)) return;
|
|
726
|
+
if (!members.has(sessionID)) return;
|
|
727
|
+
const freshness = captureFreshness();
|
|
728
|
+
setRemote(previous => {
|
|
729
|
+
if (!isCurrent(previous, freshness) || !previous.contributions) return previous;
|
|
730
|
+
const prior = previous.contributions.get(sessionID);
|
|
731
|
+
const parentID = prior?.parentID ?? parents.get(sessionID);
|
|
732
|
+
const contributions = new Map(previous.contributions);
|
|
733
|
+
contributions.set(sessionID, {
|
|
734
|
+
...(prior ?? {
|
|
735
|
+
metrics: emptyMetrics()
|
|
736
|
+
}),
|
|
737
|
+
totals: totalsFromUsage(tokens, cost),
|
|
738
|
+
...(parentID ? {
|
|
739
|
+
parentID
|
|
740
|
+
} : {})
|
|
741
|
+
});
|
|
742
|
+
markRevised([sessionID]);
|
|
743
|
+
failedMembers.delete(sessionID);
|
|
744
|
+
return publishFamily(freshness.sessionID, contributions, previous.incompleteBranches);
|
|
745
|
+
});
|
|
746
|
+
retryIncompleteBranches();
|
|
691
747
|
});
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
748
|
+
const offStepStarted = api.data.on("session.step.started", event => {
|
|
749
|
+
const {
|
|
750
|
+
sessionID,
|
|
751
|
+
assistantMessageID,
|
|
752
|
+
started
|
|
753
|
+
} = event.data;
|
|
754
|
+
if (members.has(sessionID)) firstOutputs.delete(assistantMessageID);
|
|
755
|
+
if (sessionID !== sessionId()) return;
|
|
756
|
+
if (ttftTurnShown) return;
|
|
757
|
+
ttftTurnShown = true;
|
|
758
|
+
setLiveTtft({
|
|
759
|
+
messageID: assistantMessageID,
|
|
760
|
+
createdAt: started,
|
|
761
|
+
now: Date.now()
|
|
762
|
+
});
|
|
763
|
+
ensureTimer();
|
|
695
764
|
});
|
|
696
|
-
const
|
|
697
|
-
if (event.
|
|
698
|
-
|
|
765
|
+
const offExecutionStarted = api.data.on("session.execution.started", event => {
|
|
766
|
+
if (event.data.sessionID !== sessionId()) return;
|
|
767
|
+
ttftTurnShown = false;
|
|
699
768
|
});
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
retryIncompleteBranches();
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
addBranch(event.properties.info);
|
|
769
|
+
const offStepStreamed = api.data.on("session.step.streamed", event => {
|
|
770
|
+
const {
|
|
771
|
+
sessionID,
|
|
772
|
+
assistantMessageID
|
|
773
|
+
} = event.data;
|
|
774
|
+
if (sessionID !== sessionId()) return;
|
|
775
|
+
if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
711
776
|
});
|
|
712
|
-
const
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
777
|
+
const onOutputStarted = event => {
|
|
778
|
+
const {
|
|
779
|
+
sessionID,
|
|
780
|
+
assistantMessageID
|
|
781
|
+
} = event.data;
|
|
782
|
+
if (!members.has(sessionID)) return;
|
|
783
|
+
if (!firstOutputs.has(assistantMessageID)) firstOutputs.set(assistantMessageID, event.created);
|
|
784
|
+
if (sessionID === sessionId() && liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
785
|
+
};
|
|
786
|
+
const offTextStarted = api.data.on("session.text.started", onOutputStarted);
|
|
787
|
+
const offReasoningStarted = api.data.on("session.reasoning.started", onOutputStarted);
|
|
788
|
+
const offToolInputStarted = api.data.on("session.tool.input.started", onOutputStarted);
|
|
789
|
+
const onStepSettled = (sessionID, assistantMessageID) => {
|
|
790
|
+
if (sessionID === sessionId()) {
|
|
791
|
+
if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
792
|
+
ttftTurnShown = true;
|
|
727
793
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
ensureTimer();
|
|
794
|
+
refreshMember(sessionID);
|
|
795
|
+
retryIncompleteBranches();
|
|
796
|
+
};
|
|
797
|
+
const offStepEnded = api.data.on("session.step.ended", event => {
|
|
798
|
+
onStepSettled(event.data.sessionID, event.data.assistantMessageID);
|
|
799
|
+
});
|
|
800
|
+
const offStepFailed = api.data.on("session.step.failed", event => {
|
|
801
|
+
onStepSettled(event.data.sessionID, event.data.assistantMessageID);
|
|
737
802
|
});
|
|
738
|
-
const
|
|
803
|
+
const onStreamDelta = (kind, stream) => {
|
|
739
804
|
const {
|
|
740
805
|
sessionID,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
field,
|
|
806
|
+
assistantMessageID,
|
|
807
|
+
ordinal,
|
|
744
808
|
delta
|
|
745
|
-
} =
|
|
746
|
-
if (sessionID !== sessionId()
|
|
747
|
-
|
|
809
|
+
} = stream;
|
|
810
|
+
if (sessionID !== sessionId()) return;
|
|
811
|
+
const partKey = `${kind}:${assistantMessageID}:${ordinal}`;
|
|
812
|
+
if (endedParts.has(partKey)) return;
|
|
748
813
|
let message;
|
|
749
814
|
try {
|
|
750
|
-
|
|
751
|
-
message = api.state.session.messages(sessionID).find(candidate => candidate.id === messageID);
|
|
815
|
+
message = api.data.session.message.list(sessionID).find(candidate => candidate.id === assistantMessageID);
|
|
752
816
|
} catch {
|
|
753
817
|
return;
|
|
754
818
|
}
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
}
|
|
819
|
+
if (message?.type !== "assistant" || message.time.completed !== undefined) return;
|
|
820
|
+
const streaming = message;
|
|
758
821
|
const now = Date.now();
|
|
759
822
|
const chars = codePoints(delta);
|
|
760
823
|
setLiveSpeed(current => {
|
|
761
|
-
if (current?.
|
|
824
|
+
if (current?.partKey === partKey) {
|
|
762
825
|
return {
|
|
763
826
|
...current,
|
|
764
827
|
chars: current.chars + chars
|
|
765
828
|
};
|
|
766
829
|
}
|
|
767
|
-
const calibration = remote()?.metrics?.calibrations.get(modelKey(
|
|
830
|
+
const calibration = remote()?.metrics?.calibrations.get(modelKey(streaming));
|
|
768
831
|
const charsPerToken = (400 + (calibration?.chars ?? 0)) / (100 + (calibration?.tokens ?? 0));
|
|
769
832
|
return {
|
|
770
|
-
messageID,
|
|
771
|
-
|
|
833
|
+
messageID: assistantMessageID,
|
|
834
|
+
partKey,
|
|
772
835
|
startedAt: now,
|
|
773
836
|
now,
|
|
774
837
|
chars,
|
|
@@ -777,32 +840,67 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
777
840
|
hasTicked: false
|
|
778
841
|
};
|
|
779
842
|
});
|
|
780
|
-
if (liveTtft()?.messageID ===
|
|
843
|
+
if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
|
|
781
844
|
ensureTimer();
|
|
845
|
+
};
|
|
846
|
+
const offTextDelta = api.data.on("session.text.delta", event => {
|
|
847
|
+
onStreamDelta("text", event.data);
|
|
848
|
+
});
|
|
849
|
+
const offReasoningDelta = api.data.on("session.reasoning.delta", event => {
|
|
850
|
+
onStreamDelta("reasoning", event.data);
|
|
851
|
+
});
|
|
852
|
+
const onStreamEnded = (kind, end) => {
|
|
853
|
+
const {
|
|
854
|
+
sessionID,
|
|
855
|
+
assistantMessageID,
|
|
856
|
+
ordinal
|
|
857
|
+
} = end;
|
|
858
|
+
if (sessionID !== sessionId()) return;
|
|
859
|
+
const partKey = `${kind}:${assistantMessageID}:${ordinal}`;
|
|
860
|
+
endedParts.add(partKey);
|
|
861
|
+
if (liveSpeed()?.partKey === partKey) clearLiveSpeed();
|
|
862
|
+
};
|
|
863
|
+
const offTextEnded = api.data.on("session.text.ended", event => {
|
|
864
|
+
onStreamEnded("text", event.data);
|
|
865
|
+
});
|
|
866
|
+
const offReasoningEnded = api.data.on("session.reasoning.ended", event => {
|
|
867
|
+
onStreamEnded("reasoning", event.data);
|
|
868
|
+
});
|
|
869
|
+
const offContentUpdated = api.data.on("session.revert.committed", event => {
|
|
870
|
+
if (event.data.sessionID === sessionId()) clearProvisional();
|
|
871
|
+
if (members.has(event.data.sessionID)) refresh(sessionId());
|
|
782
872
|
});
|
|
783
|
-
const offServerConnected = api.
|
|
873
|
+
const offServerConnected = api.data.on("server.connected", () => {
|
|
784
874
|
clearProvisional();
|
|
785
875
|
refresh(sessionId());
|
|
786
876
|
});
|
|
787
|
-
const
|
|
788
|
-
if (!event.
|
|
877
|
+
const offExecutionFailed = api.data.on("session.execution.failed", event => {
|
|
878
|
+
if (!event.data.sessionID || event.data.sessionID === sessionId()) clearProvisional();
|
|
789
879
|
});
|
|
790
|
-
const offSessionIdle = api.
|
|
791
|
-
if (event.
|
|
880
|
+
const offSessionIdle = api.data.on("session.idle", event => {
|
|
881
|
+
if (event.data.sessionID === sessionId()) clearProvisional();
|
|
792
882
|
});
|
|
793
883
|
solid.onCleanup(() => {
|
|
794
884
|
request++;
|
|
795
885
|
clearProvisional();
|
|
796
886
|
offSessionCreated();
|
|
797
887
|
offSessionDeleted();
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
888
|
+
offUsageUpdated();
|
|
889
|
+
offStepStarted();
|
|
890
|
+
offExecutionStarted();
|
|
891
|
+
offStepStreamed();
|
|
892
|
+
offTextStarted();
|
|
893
|
+
offReasoningStarted();
|
|
894
|
+
offToolInputStarted();
|
|
895
|
+
offStepEnded();
|
|
896
|
+
offStepFailed();
|
|
897
|
+
offTextDelta();
|
|
898
|
+
offReasoningDelta();
|
|
899
|
+
offTextEnded();
|
|
900
|
+
offReasoningEnded();
|
|
901
|
+
offContentUpdated();
|
|
804
902
|
offServerConnected();
|
|
805
|
-
|
|
903
|
+
offExecutionFailed();
|
|
806
904
|
offSessionIdle();
|
|
807
905
|
});
|
|
808
906
|
const snapshot = solid.createMemo(() => {
|
|
@@ -819,7 +917,7 @@ export function createUsageModel(api, sessionId, solid) {
|
|
|
819
917
|
hasDescendants: false
|
|
820
918
|
};
|
|
821
919
|
}
|
|
822
|
-
const totals = loaded?.sessionID === sessionID && loaded.totals ? loaded.totals : totalsFromSession(api.
|
|
920
|
+
const totals = loaded?.sessionID === sessionID && loaded.totals ? loaded.totals : totalsFromSession(api.data.session.get(sessionID));
|
|
823
921
|
const hasDescendants = loaded?.sessionID === sessionID ? loaded.hasDescendants : false;
|
|
824
922
|
if (!totals) {
|
|
825
923
|
const speed = liveSpeed();
|
package/dist/usage-panel.js
CHANGED
|
@@ -22,6 +22,7 @@ import { createElement as _$createElement } from "@opentui/solid";
|
|
|
22
22
|
*/
|
|
23
23
|
/** @jsxImportSource @opentui/solid */
|
|
24
24
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show, untrack } from "solid-js";
|
|
25
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
25
26
|
import { USAGE_SECTION_TITLE, USAGE_SECTION_TITLE_WITH_SUBAGENTS, USAGE_STATUS_TEXT, createUsageModel } from "./usage-model.js";
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -38,9 +39,9 @@ const solid = {
|
|
|
38
39
|
untrack
|
|
39
40
|
};
|
|
40
41
|
function Section(props) {
|
|
41
|
-
const theme = () => props.
|
|
42
|
+
const theme = () => props.context.theme;
|
|
42
43
|
const [collapsed, setCollapsed] = createSignal(false);
|
|
43
|
-
const model = createUsageModel(props.
|
|
44
|
+
const model = createUsageModel(props.context, () => props.sessionID, solid);
|
|
44
45
|
return (() => {
|
|
45
46
|
var _el$ = _$createElement("box"),
|
|
46
47
|
_el$2 = _$createElement("box"),
|
|
@@ -68,7 +69,7 @@ function Section(props) {
|
|
|
68
69
|
get children() {
|
|
69
70
|
var _el$6 = _$createElement("text");
|
|
70
71
|
_$insert(_el$6, () => USAGE_STATUS_TEXT[model.status()]);
|
|
71
|
-
_$effect(_$p => _$setProp(_el$6, "fg", theme().
|
|
72
|
+
_$effect(_$p => _$setProp(_el$6, "fg", theme().text.muted, _$p));
|
|
72
73
|
return _el$6;
|
|
73
74
|
}
|
|
74
75
|
}), _$createComponent(For, {
|
|
@@ -86,8 +87,8 @@ function Section(props) {
|
|
|
86
87
|
_$insert(_el$8, () => row.label);
|
|
87
88
|
_$insert(_el$9, () => row.value);
|
|
88
89
|
_$effect(_p$ => {
|
|
89
|
-
var _v$3 = theme().
|
|
90
|
-
_v$4 = theme().text;
|
|
90
|
+
var _v$3 = theme().text.muted,
|
|
91
|
+
_v$4 = theme().text.base;
|
|
91
92
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$8, "fg", _v$3, _p$.e));
|
|
92
93
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$9, "fg", _v$4, _p$.t));
|
|
93
94
|
return _p$;
|
|
@@ -101,8 +102,8 @@ function Section(props) {
|
|
|
101
102
|
}
|
|
102
103
|
}), null);
|
|
103
104
|
_$effect(_p$ => {
|
|
104
|
-
var _v$ = theme().text,
|
|
105
|
-
_v$2 = theme().text;
|
|
105
|
+
var _v$ = theme().text.base,
|
|
106
|
+
_v$2 = theme().text.base;
|
|
106
107
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
|
|
107
108
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
|
|
108
109
|
return _p$;
|
|
@@ -113,25 +114,21 @@ function Section(props) {
|
|
|
113
114
|
return _el$;
|
|
114
115
|
})();
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
// Order 150: internal sidebar sections sit at 100/200/300/400/500, so this
|
|
118
|
-
// lands right after the first block without moving any existing section.
|
|
119
|
-
api.slots.register({
|
|
120
|
-
order: 150,
|
|
121
|
-
slots: {
|
|
122
|
-
sidebar_content(_ctx, props) {
|
|
123
|
-
return _$createComponent(Section, {
|
|
124
|
-
api: api,
|
|
125
|
-
get session_id() {
|
|
126
|
-
return props.session_id;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
const plugin = {
|
|
117
|
+
export default Plugin.define({
|
|
134
118
|
id: "supercode.token-usage",
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.1
|
|
4
|
+
"version": "1.0.1",
|
|
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
|
}
|