opencode-tui-stats 0.4.1 → 0.4.3

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/dist/tui.js ADDED
@@ -0,0 +1,949 @@
1
+ // @bun
2
+ // tui.tsx
3
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
5
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
10
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
11
+ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "opentui:runtime-module:solid-js";
12
+
13
+ // tracker.ts
14
+ var STREAM_WINDOW_MS = 5000;
15
+ var LIVE_STALE_MS = 1500;
16
+ var SINGLE_SAMPLE_MS = 1000;
17
+ var PART_CACHE_LIMIT = 1024;
18
+ var DEDUP_WINDOW_LIMIT = 64;
19
+ var MESSAGE_MODEL_LIMIT = 256;
20
+ var SESSION_LIMIT = 64;
21
+ var SUBTREE_LIMIT = 64;
22
+ var MODEL_ROW_LIMIT = 4;
23
+ function emptyStats() {
24
+ return {
25
+ cacheRead: 0,
26
+ cachePrompt: 0,
27
+ cost: 0,
28
+ steps: 0,
29
+ retries: 0,
30
+ lastAttempt: 0
31
+ };
32
+ }
33
+ function emptyAggregate() {
34
+ return {
35
+ cacheRead: 0,
36
+ cachePrompt: 0,
37
+ cost: 0,
38
+ steps: 0,
39
+ totalTokens: 0,
40
+ totalDurationMs: 0,
41
+ totalTtftMs: 0,
42
+ stepCount: 0,
43
+ lastAt: 0
44
+ };
45
+ }
46
+ function modelKey(providerID, modelID) {
47
+ return `${providerID}/${modelID}`;
48
+ }
49
+ function estimateTokens(delta) {
50
+ return Math.max(1, Math.ceil(Buffer.byteLength(delta, "utf8") / 5));
51
+ }
52
+ function formatRate(value) {
53
+ if (!Number.isFinite(value) || value <= 0)
54
+ return;
55
+ if (value >= 100)
56
+ return Math.round(value).toString();
57
+ if (value >= 10)
58
+ return value.toFixed(1);
59
+ return value.toFixed(2);
60
+ }
61
+ function formatTtft(value) {
62
+ if (!Number.isFinite(value) || value < 0)
63
+ return;
64
+ return `${value.toFixed(1)}s`;
65
+ }
66
+ function activeDuration(samples, tailAt) {
67
+ if (samples.length === 0)
68
+ return 0;
69
+ if (samples.length === 1) {
70
+ const tailDuration = tailAt ? Math.max(0, tailAt - samples[0].at) : SINGLE_SAMPLE_MS;
71
+ return Math.min(Math.max(tailDuration, 250), SINGLE_SAMPLE_MS);
72
+ }
73
+ let duration = 0;
74
+ for (let index = 1;index < samples.length; index++) {
75
+ duration += Math.max(0, samples[index].at - samples[index - 1].at);
76
+ }
77
+ if (tailAt)
78
+ duration += Math.max(0, tailAt - samples[samples.length - 1].at);
79
+ return Math.max(duration, SINGLE_SAMPLE_MS);
80
+ }
81
+ function averageRate(average) {
82
+ if (!average || average.totalTokens <= 0 || average.totalDurationMs <= 0)
83
+ return;
84
+ return formatRate(average.totalTokens / (average.totalDurationMs / 1000));
85
+ }
86
+ function averageTtft(average) {
87
+ if (!average || average.stepCount <= 0)
88
+ return;
89
+ return formatTtft(average.totalTtftMs / average.stepCount / 1000);
90
+ }
91
+ function cacheText(stats) {
92
+ if (!stats || stats.cachePrompt <= 0)
93
+ return;
94
+ return `${(stats.cacheRead / stats.cachePrompt * 100).toFixed(1)}%`;
95
+ }
96
+ function costText(stats) {
97
+ if (!stats || stats.steps <= 0)
98
+ return;
99
+ return `$${stats.cost.toFixed(4)}`;
100
+ }
101
+ function boundedAdd(set, key, limit) {
102
+ if (set.has(key))
103
+ return;
104
+ if (set.size >= limit) {
105
+ const oldest = set.values().next().value;
106
+ if (oldest !== undefined)
107
+ set.delete(oldest);
108
+ }
109
+ set.add(key);
110
+ }
111
+ function boundedSet(map, key, value, limit) {
112
+ if (map.has(key))
113
+ return;
114
+ if (map.size >= limit) {
115
+ const oldest = map.keys().next().value;
116
+ if (oldest !== undefined)
117
+ map.delete(oldest);
118
+ }
119
+ map.set(key, value);
120
+ }
121
+ var lastInternals;
122
+ function createTracker(api, onChange) {
123
+ const tracker = {
124
+ samples: {},
125
+ timings: {},
126
+ averages: {},
127
+ stats: {},
128
+ dedup: {},
129
+ models: {},
130
+ parentOf: {}
131
+ };
132
+ const partDecisions = new Map;
133
+ const mounted = new Set;
134
+ const order = [];
135
+ const listeners = [];
136
+ const bump = () => onChange();
137
+ const clearLive = (sessionID) => {
138
+ if (!tracker.samples[sessionID]?.length)
139
+ return;
140
+ delete tracker.samples[sessionID];
141
+ bump();
142
+ };
143
+ const clearTimings = (sessionID) => {
144
+ let changed = false;
145
+ for (const [messageID, timing] of Object.entries(tracker.timings)) {
146
+ if (timing.sessionID !== sessionID)
147
+ continue;
148
+ delete tracker.timings[messageID];
149
+ changed = true;
150
+ }
151
+ if (changed)
152
+ bump();
153
+ };
154
+ const evictSession = (sessionID) => {
155
+ delete tracker.stats[sessionID];
156
+ delete tracker.averages[sessionID];
157
+ delete tracker.samples[sessionID];
158
+ delete tracker.dedup[sessionID];
159
+ delete tracker.models[sessionID];
160
+ delete tracker.parentOf[sessionID];
161
+ clearTimings(sessionID);
162
+ };
163
+ const touchSession = (sessionID) => {
164
+ const at = order.indexOf(sessionID);
165
+ if (at !== -1)
166
+ order.splice(at, 1);
167
+ order.push(sessionID);
168
+ let index = 0;
169
+ while (order.length > SESSION_LIMIT && index < order.length) {
170
+ const candidate = order[index];
171
+ if (mounted.has(candidate)) {
172
+ index += 1;
173
+ continue;
174
+ }
175
+ order.splice(index, 1);
176
+ evictSession(candidate);
177
+ }
178
+ };
179
+ const touchModel = (models, key) => {
180
+ const at = models.order.indexOf(key);
181
+ if (at !== -1)
182
+ models.order.splice(at, 1);
183
+ models.order.push(key);
184
+ if (!models.aggregates[key])
185
+ models.aggregates[key] = emptyAggregate();
186
+ };
187
+ const prune = (now = Date.now()) => {
188
+ let changed = false;
189
+ for (const [sessionID, samples] of Object.entries(tracker.samples)) {
190
+ const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
191
+ if (next.length === samples.length)
192
+ continue;
193
+ changed = true;
194
+ if (next.length > 0)
195
+ tracker.samples[sessionID] = next;
196
+ else
197
+ delete tracker.samples[sessionID];
198
+ }
199
+ if (changed)
200
+ bump();
201
+ };
202
+ const appendSample = (sessionID, messageID, sample) => {
203
+ touchSession(sessionID);
204
+ tracker.samples[sessionID] = [...(tracker.samples[sessionID] ?? []).filter((item) => sample.at - item.at <= STREAM_WINDOW_MS), sample];
205
+ const timing = tracker.timings[messageID];
206
+ if (timing && timing.firstResponseAt === undefined)
207
+ timing.firstResponseAt = sample.at;
208
+ bump();
209
+ };
210
+ const sessionLiveRate = (sessionID, key) => {
211
+ const frozen = {
212
+ value: tracker.models[sessionID]?.lastRate[key],
213
+ live: false
214
+ };
215
+ const status = api.state.session.status(sessionID);
216
+ if (!status || status.type === "idle")
217
+ return frozen;
218
+ const now = Date.now();
219
+ const mapping = tracker.models[sessionID]?.messageModels;
220
+ const samples = (tracker.samples[sessionID] ?? []).filter((sample) => now - sample.at <= STREAM_WINDOW_MS && mapping?.get(sample.messageID) === key);
221
+ const last = samples[samples.length - 1];
222
+ if (!last || now - last.at > LIVE_STALE_MS)
223
+ return frozen;
224
+ const tokens = samples.reduce((sum, sample) => sum + sample.tokens, 0);
225
+ const value = formatRate(tokens / (activeDuration(samples, now) / 1000));
226
+ if (value === undefined)
227
+ return frozen;
228
+ const models = tracker.models[sessionID];
229
+ if (models)
230
+ models.lastRate[key] = value;
231
+ return {
232
+ value,
233
+ live: true
234
+ };
235
+ };
236
+ const resolveParent = (sessionID) => {
237
+ if (sessionID in tracker.parentOf)
238
+ return;
239
+ const info = api.state.session.get(sessionID);
240
+ if (!info)
241
+ return;
242
+ tracker.parentOf[sessionID] = info.parentID;
243
+ };
244
+ const rememberParent = (sessionID, parentID) => {
245
+ if (sessionID in tracker.parentOf && tracker.parentOf[sessionID] === parentID)
246
+ return;
247
+ tracker.parentOf[sessionID] = parentID;
248
+ bump();
249
+ };
250
+ const knownSessions = () => {
251
+ const ids = new Set(Object.keys(tracker.models));
252
+ for (const id of Object.keys(tracker.stats))
253
+ ids.add(id);
254
+ for (const id of Object.keys(tracker.averages))
255
+ ids.add(id);
256
+ return ids;
257
+ };
258
+ const descendants = (sessionID) => {
259
+ const children = new Map;
260
+ for (const [child, parent] of Object.entries(tracker.parentOf)) {
261
+ if (!parent)
262
+ continue;
263
+ const siblings = children.get(parent);
264
+ if (siblings)
265
+ siblings.push(child);
266
+ else
267
+ children.set(parent, [child]);
268
+ }
269
+ const seen = new Set([sessionID]);
270
+ const found = [];
271
+ const queue = [sessionID];
272
+ while (queue.length > 0 && found.length < SUBTREE_LIMIT) {
273
+ const current = queue.shift();
274
+ for (const child of children.get(current) ?? []) {
275
+ if (seen.has(child))
276
+ continue;
277
+ seen.add(child);
278
+ found.push(child);
279
+ queue.push(child);
280
+ if (found.length >= SUBTREE_LIMIT)
281
+ break;
282
+ }
283
+ }
284
+ return found;
285
+ };
286
+ const mergeAggregate = (target, source) => {
287
+ target.cacheRead += source.cacheRead;
288
+ target.cachePrompt += source.cachePrompt;
289
+ target.cost += source.cost;
290
+ target.steps += source.steps;
291
+ target.totalTokens += source.totalTokens;
292
+ target.totalDurationMs += source.totalDurationMs;
293
+ target.totalTtftMs += source.totalTtftMs;
294
+ target.stepCount += source.stepCount;
295
+ target.lastAt = Math.max(target.lastAt, source.lastAt);
296
+ };
297
+ const sessionModelAt = (sessionID) => {
298
+ const models = tracker.models[sessionID];
299
+ if (!models)
300
+ return 0;
301
+ let at = 0;
302
+ for (const key of models.order) {
303
+ const aggregate = models.aggregates[key];
304
+ if (aggregate && aggregate.lastAt > at)
305
+ at = aggregate.lastAt;
306
+ }
307
+ return at;
308
+ };
309
+ const liveRateFor = (sessions, key) => {
310
+ let latest;
311
+ for (const sessionID of sessions) {
312
+ const aggregate = tracker.models[sessionID]?.aggregates[key];
313
+ if (!aggregate)
314
+ continue;
315
+ if (!latest || aggregate.lastAt >= latest.at)
316
+ latest = {
317
+ sessionID,
318
+ at: aggregate.lastAt
319
+ };
320
+ }
321
+ if (!latest)
322
+ return {
323
+ live: false
324
+ };
325
+ return sessionLiveRate(latest.sessionID, key);
326
+ };
327
+ const modelRows = (sessionID) => {
328
+ for (const id of knownSessions())
329
+ resolveParent(id);
330
+ const subtree = [sessionID, ...descendants(sessionID)];
331
+ const retries = tracker.stats[sessionID]?.retries ?? 0;
332
+ let cacheRead = 0;
333
+ let cachePrompt = 0;
334
+ for (const id of subtree) {
335
+ const stats = tracker.stats[id];
336
+ if (!stats)
337
+ continue;
338
+ cacheRead += stats.cacheRead;
339
+ cachePrompt += stats.cachePrompt;
340
+ }
341
+ const cache = cacheText({
342
+ cacheRead,
343
+ cachePrompt
344
+ });
345
+ const sessions = subtree.filter((id) => tracker.models[id]).sort((left, right) => sessionModelAt(left) - sessionModelAt(right));
346
+ if (sessions.length === 0)
347
+ return {
348
+ rows: [],
349
+ overflow: 0,
350
+ retries,
351
+ cache
352
+ };
353
+ const order2 = [];
354
+ const aggregates = {};
355
+ const subagentModels = new Set;
356
+ for (const id of sessions) {
357
+ const models = tracker.models[id];
358
+ for (const key of models.order) {
359
+ const aggregate = models.aggregates[key];
360
+ if (!aggregate)
361
+ continue;
362
+ let merged = aggregates[key];
363
+ if (!merged) {
364
+ merged = emptyAggregate();
365
+ aggregates[key] = merged;
366
+ order2.push(key);
367
+ }
368
+ mergeAggregate(merged, aggregate);
369
+ if (id !== sessionID)
370
+ subagentModels.add(key);
371
+ }
372
+ }
373
+ let active;
374
+ for (const id of sessions)
375
+ active = tracker.models[id].active ?? active;
376
+ const recency = [...order2].reverse();
377
+ const ordered = active && active in aggregates ? [active, ...recency.filter((key) => key !== active)] : recency;
378
+ const visible = ordered.slice(0, MODEL_ROW_LIMIT);
379
+ const overflow = Math.max(0, ordered.length - visible.length);
380
+ const rows = visible.map((key) => {
381
+ const aggregate = aggregates[key];
382
+ const isActive = key === active;
383
+ const tps = isActive ? liveRateFor(subtree, key) : {
384
+ value: undefined,
385
+ live: false
386
+ };
387
+ return {
388
+ key,
389
+ label: key,
390
+ active: isActive,
391
+ subagent: subagentModels.has(key),
392
+ tps: tps.value,
393
+ tpsLive: tps.live,
394
+ avg: averageRate(aggregate),
395
+ ttft: averageTtft(aggregate),
396
+ cache: cacheText(aggregate),
397
+ cost: costText(aggregate)
398
+ };
399
+ });
400
+ return {
401
+ rows,
402
+ overflow,
403
+ retries,
404
+ cache
405
+ };
406
+ };
407
+ listeners.push(api.event.on("message.part.delta", (event) => {
408
+ if (event.properties.field !== "text")
409
+ return;
410
+ const {
411
+ sessionID,
412
+ messageID,
413
+ partID
414
+ } = event.properties;
415
+ let counted = partDecisions.get(partID);
416
+ if (counted === undefined) {
417
+ const part = api.state.part(messageID).find((item) => item.id === partID);
418
+ if (!part)
419
+ return;
420
+ counted = part.type === "text" || part.type === "reasoning";
421
+ boundedSet(partDecisions, partID, counted, PART_CACHE_LIMIT);
422
+ }
423
+ if (!counted)
424
+ return;
425
+ appendSample(sessionID, messageID, {
426
+ at: Date.now(),
427
+ messageID,
428
+ tokens: estimateTokens(event.properties.delta)
429
+ });
430
+ }));
431
+ listeners.push(api.event.on("message.updated", (event) => {
432
+ const info = event.properties.info;
433
+ if (info.role !== "assistant")
434
+ return;
435
+ const sessionID = event.properties.sessionID;
436
+ const key = modelKey(info.providerID, info.modelID);
437
+ const models = tracker.models[sessionID] ?? (tracker.models[sessionID] = {
438
+ order: [],
439
+ aggregates: {},
440
+ messageModels: new Map,
441
+ lastRate: {}
442
+ });
443
+ touchModel(models, key);
444
+ models.active = key;
445
+ models.aggregates[key] = {
446
+ ...models.aggregates[key],
447
+ lastAt: Date.now()
448
+ };
449
+ boundedSet(models.messageModels, info.id, key, MESSAGE_MODEL_LIMIT);
450
+ if (!info.time.completed) {
451
+ const existing = tracker.timings[info.id];
452
+ tracker.timings[info.id] = {
453
+ sessionID,
454
+ requestStartAt: info.time.created,
455
+ firstResponseAt: existing?.firstResponseAt,
456
+ lastToolCallAt: existing?.lastToolCallAt
457
+ };
458
+ bump();
459
+ return;
460
+ }
461
+ const seen = tracker.dedup[sessionID] ?? (tracker.dedup[sessionID] = new Set);
462
+ if (!seen.has(info.id)) {
463
+ boundedAdd(seen, info.id, DEDUP_WINDOW_LIMIT);
464
+ touchSession(sessionID);
465
+ const stats = tracker.stats[sessionID] ?? emptyStats();
466
+ const cacheRead = info.tokens.cache.read;
467
+ const cacheWrite = info.tokens.cache.write;
468
+ tracker.stats[sessionID] = {
469
+ ...stats,
470
+ cacheRead: stats.cacheRead + cacheRead,
471
+ cachePrompt: stats.cachePrompt + info.tokens.input + cacheRead + cacheWrite,
472
+ cost: stats.cost + info.cost,
473
+ steps: stats.steps + 1
474
+ };
475
+ const aggregate = models.aggregates[key] ?? emptyAggregate();
476
+ models.aggregates[key] = {
477
+ ...aggregate,
478
+ cacheRead: aggregate.cacheRead + cacheRead,
479
+ cachePrompt: aggregate.cachePrompt + info.tokens.input + cacheRead + cacheWrite,
480
+ cost: aggregate.cost + info.cost,
481
+ steps: aggregate.steps + 1
482
+ };
483
+ }
484
+ const timing = tracker.timings[info.id];
485
+ if (timing?.sessionID === sessionID && timing.firstResponseAt !== undefined) {
486
+ const tokens = info.tokens.output + info.tokens.reasoning;
487
+ const endAt = info.finish === "tool-calls" ? timing.lastToolCallAt : info.time.completed;
488
+ const durationMs = endAt !== undefined ? Math.max(endAt - timing.firstResponseAt, 1) : undefined;
489
+ if (tokens > 0 && durationMs !== undefined) {
490
+ const totals = tracker.averages[sessionID] ?? {
491
+ totalTokens: 0,
492
+ totalDurationMs: 0,
493
+ totalTtftMs: 0,
494
+ stepCount: 0
495
+ };
496
+ tracker.averages[sessionID] = {
497
+ totalTokens: totals.totalTokens + tokens,
498
+ totalDurationMs: totals.totalDurationMs + durationMs,
499
+ totalTtftMs: totals.totalTtftMs + Math.max(timing.firstResponseAt - timing.requestStartAt, 0),
500
+ stepCount: totals.stepCount + 1
501
+ };
502
+ const aggregate = models.aggregates[key] ?? emptyAggregate();
503
+ models.aggregates[key] = {
504
+ ...aggregate,
505
+ totalTokens: aggregate.totalTokens + tokens,
506
+ totalDurationMs: aggregate.totalDurationMs + durationMs,
507
+ totalTtftMs: aggregate.totalTtftMs + Math.max(timing.firstResponseAt - timing.requestStartAt, 0),
508
+ stepCount: aggregate.stepCount + 1
509
+ };
510
+ }
511
+ }
512
+ delete tracker.timings[info.id];
513
+ prune(info.time.completed);
514
+ bump();
515
+ }));
516
+ listeners.push(api.event.on("message.part.updated", (event) => {
517
+ const part = event.properties.part;
518
+ if (part.type !== "tool")
519
+ return;
520
+ if (part.state.status === "running" || part.state.status === "completed" || part.state.status === "error") {
521
+ clearLive(part.sessionID);
522
+ }
523
+ const timing = tracker.timings[part.messageID];
524
+ if (!timing)
525
+ return;
526
+ if (part.state.status === "pending") {
527
+ if (timing.firstResponseAt === undefined)
528
+ timing.firstResponseAt = Date.now();
529
+ bump();
530
+ return;
531
+ }
532
+ if (part.state.status === "running") {
533
+ timing.lastToolCallAt = part.state.time.start;
534
+ bump();
535
+ }
536
+ }));
537
+ listeners.push(api.event.on("session.status", (event) => {
538
+ const {
539
+ sessionID,
540
+ status
541
+ } = event.properties;
542
+ const type = status.type;
543
+ if (type === "idle")
544
+ clearTimings(sessionID);
545
+ const stats = tracker.stats[sessionID] ?? emptyStats();
546
+ if (type === "retry") {
547
+ const attempt = status.attempt;
548
+ if (attempt <= stats.lastAttempt)
549
+ return;
550
+ touchSession(sessionID);
551
+ tracker.stats[sessionID] = {
552
+ ...stats,
553
+ retries: stats.retries + (attempt - stats.lastAttempt),
554
+ lastAttempt: attempt
555
+ };
556
+ bump();
557
+ return;
558
+ }
559
+ if (stats.lastAttempt === 0)
560
+ return;
561
+ touchSession(sessionID);
562
+ tracker.stats[sessionID] = {
563
+ ...stats,
564
+ lastAttempt: 0
565
+ };
566
+ bump();
567
+ }));
568
+ listeners.push(api.event.on("session.idle", (event) => {
569
+ clearTimings(event.properties.sessionID);
570
+ }));
571
+ listeners.push(api.event.on("session.created", (event) => {
572
+ rememberParent(event.properties.info.id, event.properties.info.parentID);
573
+ }));
574
+ listeners.push(api.event.on("session.updated", (event) => {
575
+ rememberParent(event.properties.info.id, event.properties.info.parentID);
576
+ }));
577
+ const timer = setInterval(() => {
578
+ prune();
579
+ bump();
580
+ }, 1000);
581
+ const internals = {
582
+ sizes: () => ({
583
+ parts: partDecisions.size,
584
+ dedup: Object.values(tracker.dedup).reduce((sum, set) => sum + set.size, 0),
585
+ stats: Object.keys(tracker.stats).length,
586
+ averages: Object.keys(tracker.averages).length,
587
+ timings: Object.keys(tracker.timings).length,
588
+ mounted: mounted.size
589
+ }),
590
+ partDecision: (partID) => partDecisions.get(partID),
591
+ hasStats: (sessionID) => (sessionID in tracker.stats),
592
+ dedupSize: (sessionID) => tracker.dedup[sessionID]?.size ?? 0,
593
+ retries: (sessionID) => tracker.stats[sessionID]?.retries ?? 0,
594
+ mounted: () => [...mounted],
595
+ activeModel: (sessionID) => tracker.models[sessionID]?.active
596
+ };
597
+ lastInternals = internals;
598
+ return {
599
+ modelRows,
600
+ mountSession(sessionID) {
601
+ mounted.add(sessionID);
602
+ },
603
+ unmountSession(sessionID) {
604
+ mounted.delete(sessionID);
605
+ },
606
+ prune,
607
+ internals,
608
+ dispose() {
609
+ for (const unsubscribe of listeners)
610
+ unsubscribe();
611
+ listeners.length = 0;
612
+ clearInterval(timer);
613
+ mounted.clear();
614
+ if (lastInternals === internals)
615
+ lastInternals = undefined;
616
+ }
617
+ };
618
+ }
619
+ var lastPanelController;
620
+ function setLastPanelController(controller) {
621
+ lastPanelController = controller;
622
+ }
623
+ function clearLastPanelController(controller) {
624
+ if (lastPanelController === controller)
625
+ lastPanelController = undefined;
626
+ }
627
+
628
+ // tui.tsx
629
+ var DETAIL_INDENT = " ";
630
+ var RATE_CELL = 4;
631
+ var TTFT_CELL = 6;
632
+ var CACHE_CELL = 6;
633
+ function splitLabel(label) {
634
+ const slash = label.indexOf("/");
635
+ if (slash <= 0)
636
+ return {
637
+ provider: "",
638
+ model: label
639
+ };
640
+ const provider = label.slice(0, slash);
641
+ const model = label.slice(slash + 1);
642
+ if (model.toLowerCase().startsWith(provider.toLowerCase()))
643
+ return {
644
+ provider: "",
645
+ model
646
+ };
647
+ return {
648
+ provider: `${provider}/`,
649
+ model
650
+ };
651
+ }
652
+ function rateCell(value) {
653
+ return value === undefined ? "\u2014" : `${value.padStart(RATE_CELL)}t/s`;
654
+ }
655
+ function ttftCell(value) {
656
+ return (value ?? "\u2014").padEnd(TTFT_CELL);
657
+ }
658
+ function cacheCell(value) {
659
+ if (value === undefined)
660
+ return "\u2014".padStart(CACHE_CELL);
661
+ const percent = value.endsWith("%") ? value.slice(0, -1) : value;
662
+ return `${percent.padStart(5)}%`;
663
+ }
664
+ function ModelRowView(props) {
665
+ let pressed;
666
+ const handleMouseDown = (event) => {
667
+ pressed = {
668
+ x: event.x,
669
+ y: event.y
670
+ };
671
+ };
672
+ const handleMouseUp = (event) => {
673
+ const start = pressed;
674
+ pressed = undefined;
675
+ if (start && (start.x !== event.x || start.y !== event.y))
676
+ return;
677
+ props.onToggle();
678
+ };
679
+ const theme = () => props.api.theme.current;
680
+ const parts = () => splitLabel(props.row.label);
681
+ return (() => {
682
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("span"), _el$4 = _$createElement("span"), _el$8 = _$createElement("span");
683
+ _$insertNode(_el$, _el$2);
684
+ _$setProp(_el$, "flexDirection", "column");
685
+ _$setProp(_el$, "onMouseDown", handleMouseDown);
686
+ _$setProp(_el$, "onMouseUp", handleMouseUp);
687
+ _$insertNode(_el$2, _el$3);
688
+ _$insertNode(_el$2, _el$4);
689
+ _$insertNode(_el$2, _el$8);
690
+ _$setProp(_el$2, "wrapMode", "none");
691
+ _$setProp(_el$2, "truncate", true);
692
+ _$insert(_el$3, () => props.row.active ? "\u25CF " : " ");
693
+ _$insert(_el$4, () => `${props.expanded ? "\u25BC" : "\u25B6"} `);
694
+ _$insert(_el$2, _$createComponent(Show, {
695
+ get when() {
696
+ return props.row.subagent;
697
+ },
698
+ get children() {
699
+ var _el$5 = _$createElement("span");
700
+ _$insertNode(_el$5, _$createTextNode(`\u21B3 `));
701
+ _$effect((_$p) => _$setProp(_el$5, "style", {
702
+ fg: theme().textMuted
703
+ }, _$p));
704
+ return _el$5;
705
+ }
706
+ }), _el$8);
707
+ _$insert(_el$2, _$createComponent(Show, {
708
+ get when() {
709
+ return parts().provider;
710
+ },
711
+ get children() {
712
+ var _el$7 = _$createElement("span");
713
+ _$insert(_el$7, () => parts().provider);
714
+ _$effect((_$p) => _$setProp(_el$7, "style", {
715
+ fg: theme().text,
716
+ dim: true
717
+ }, _$p));
718
+ return _el$7;
719
+ }
720
+ }), _el$8);
721
+ _$insert(_el$8, () => parts().model);
722
+ _$insert(_el$, _$createComponent(Show, {
723
+ get when() {
724
+ return props.expanded;
725
+ },
726
+ get children() {
727
+ return [(() => {
728
+ var _el$9 = _$createElement("text"), _el$0 = _$createElement("span");
729
+ _$insertNode(_el$9, _el$0);
730
+ _$setProp(_el$9, "wrapMode", "none");
731
+ _$setProp(_el$9, "truncate", true);
732
+ _$insert(_el$0, () => `${DETAIL_INDENT}AVG ${rateCell(props.row.avg)}`);
733
+ _$insert(_el$9, _$createComponent(Show, {
734
+ get when() {
735
+ return props.row.tps !== undefined;
736
+ },
737
+ get children() {
738
+ return [(() => {
739
+ var _el$1 = _$createElement("span");
740
+ _$insertNode(_el$1, _$createTextNode(` \xB7 TPS `));
741
+ _$effect((_$p) => _$setProp(_el$1, "style", {
742
+ fg: theme().textMuted
743
+ }, _$p));
744
+ return _el$1;
745
+ })(), (() => {
746
+ var _el$11 = _$createElement("span");
747
+ _$insert(_el$11, () => rateCell(props.row.tps));
748
+ _$effect((_$p) => _$setProp(_el$11, "style", {
749
+ fg: theme().textMuted,
750
+ dim: !props.row.tpsLive
751
+ }, _$p));
752
+ return _el$11;
753
+ })()];
754
+ }
755
+ }), null);
756
+ _$effect((_p$) => {
757
+ var _v$ = theme().textMuted, _v$2 = {
758
+ fg: theme().textMuted
759
+ };
760
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$9, "fg", _v$, _p$.e));
761
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$0, "style", _v$2, _p$.t));
762
+ return _p$;
763
+ }, {
764
+ e: undefined,
765
+ t: undefined
766
+ });
767
+ return _el$9;
768
+ })(), (() => {
769
+ var _el$12 = _$createElement("text");
770
+ _$setProp(_el$12, "wrapMode", "none");
771
+ _$setProp(_el$12, "truncate", true);
772
+ _$insert(_el$12, () => `${DETAIL_INDENT}TTFT ${ttftCell(props.row.ttft)} \xB7 CACHE ${cacheCell(props.row.cache)}`);
773
+ _$effect((_$p) => _$setProp(_el$12, "fg", theme().textMuted, _$p));
774
+ return _el$12;
775
+ })(), (() => {
776
+ var _el$13 = _$createElement("text");
777
+ _$setProp(_el$13, "wrapMode", "none");
778
+ _$setProp(_el$13, "truncate", true);
779
+ _$insert(_el$13, () => `${DETAIL_INDENT}${props.row.cost ?? "\u2014"}`);
780
+ _$effect((_$p) => _$setProp(_el$13, "fg", theme().textMuted, _$p));
781
+ return _el$13;
782
+ })()];
783
+ }
784
+ }), null);
785
+ _$effect((_p$) => {
786
+ var _v$3 = {
787
+ fg: theme().success
788
+ }, _v$4 = {
789
+ fg: theme().textMuted
790
+ }, _v$5 = {
791
+ fg: theme().text
792
+ };
793
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$3, "style", _v$3, _p$.e));
794
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$4, "style", _v$4, _p$.t));
795
+ _v$5 !== _p$.a && (_p$.a = _$setProp(_el$8, "style", _v$5, _p$.a));
796
+ return _p$;
797
+ }, {
798
+ e: undefined,
799
+ t: undefined,
800
+ a: undefined
801
+ });
802
+ return _el$;
803
+ })();
804
+ }
805
+ function ModelPanelView(props) {
806
+ let mounted;
807
+ const initialActive = props.tracker.internals.activeModel(props.sessionID);
808
+ const [expanded, setExpanded] = createSignal(new Set(initialActive ? [initialActive] : []));
809
+ let seededActive = initialActive;
810
+ let lastSession = props.sessionID;
811
+ const panel = createMemo(() => {
812
+ props.revision();
813
+ const sessionID = props.sessionID;
814
+ if (mounted !== sessionID) {
815
+ if (mounted !== undefined)
816
+ props.tracker.unmountSession(mounted);
817
+ props.tracker.mountSession(sessionID);
818
+ mounted = sessionID;
819
+ }
820
+ return props.tracker.modelRows(sessionID);
821
+ });
822
+ createEffect(() => {
823
+ props.revision();
824
+ const sessionID = props.sessionID;
825
+ const active = props.tracker.internals.activeModel(sessionID);
826
+ if (sessionID !== lastSession) {
827
+ lastSession = sessionID;
828
+ seededActive = active;
829
+ setExpanded(new Set(active ? [active] : []));
830
+ return;
831
+ }
832
+ if (active && active !== seededActive) {
833
+ seededActive = active;
834
+ setExpanded(new Set([active]));
835
+ }
836
+ });
837
+ const toggle = (key) => {
838
+ setExpanded((prev) => {
839
+ const next = new Set(prev);
840
+ if (next.has(key))
841
+ next.delete(key);
842
+ else
843
+ next.add(key);
844
+ return next;
845
+ });
846
+ props.api.renderer?.requestRender?.();
847
+ };
848
+ const controller = {
849
+ toggle,
850
+ isExpanded: (key) => expanded().has(key)
851
+ };
852
+ setLastPanelController(controller);
853
+ onCleanup(() => {
854
+ if (mounted !== undefined)
855
+ props.tracker.unmountSession(mounted);
856
+ clearLastPanelController(controller);
857
+ });
858
+ return (() => {
859
+ var _el$14 = _$createElement("box"), _el$15 = _$createElement("text"), _el$16 = _$createElement("b"), _el$21 = _$createElement("text");
860
+ _$insertNode(_el$14, _el$15);
861
+ _$insertNode(_el$14, _el$21);
862
+ _$setProp(_el$14, "flexDirection", "column");
863
+ _$insertNode(_el$15, _el$16);
864
+ _$setProp(_el$15, "wrapMode", "none");
865
+ _$setProp(_el$15, "truncate", true);
866
+ _$insertNode(_el$16, _$createTextNode(`Model-Performance \xB7 observed`));
867
+ _$insert(_el$14, _$createComponent(Show, {
868
+ get when() {
869
+ return panel().rows.length === 0;
870
+ },
871
+ get children() {
872
+ var _el$18 = _$createElement("text");
873
+ _$insertNode(_el$18, _$createTextNode(`\u2014`));
874
+ _$setProp(_el$18, "wrapMode", "none");
875
+ _$setProp(_el$18, "truncate", true);
876
+ _$effect((_$p) => _$setProp(_el$18, "fg", props.api.theme.current.textMuted, _$p));
877
+ return _el$18;
878
+ }
879
+ }), _el$21);
880
+ _$insert(_el$14, _$createComponent(For, {
881
+ get each() {
882
+ return panel().rows;
883
+ },
884
+ children: (row) => _$createComponent(ModelRowView, {
885
+ get api() {
886
+ return props.api;
887
+ },
888
+ row,
889
+ get expanded() {
890
+ return expanded().has(row.key);
891
+ },
892
+ onToggle: () => toggle(row.key)
893
+ })
894
+ }), _el$21);
895
+ _$insert(_el$14, _$createComponent(Show, {
896
+ get when() {
897
+ return panel().overflow > 0;
898
+ },
899
+ get children() {
900
+ var _el$20 = _$createElement("text");
901
+ _$setProp(_el$20, "wrapMode", "none");
902
+ _$setProp(_el$20, "truncate", true);
903
+ _$insert(_el$20, () => `+${panel().overflow} more models`);
904
+ _$effect((_$p) => _$setProp(_el$20, "fg", props.api.theme.current.textMuted, _$p));
905
+ return _el$20;
906
+ }
907
+ }), _el$21);
908
+ _$setProp(_el$21, "wrapMode", "none");
909
+ _$setProp(_el$21, "truncate", true);
910
+ _$insert(_el$21, () => `Session CACHE ${panel().cache ?? "\u2014"}${panel().retries > 0 ? ` \xB7 retries ${panel().retries}` : ""}`);
911
+ _$effect((_p$) => {
912
+ var _v$6 = props.api.theme.current.text, _v$7 = props.api.theme.current.textMuted;
913
+ _v$6 !== _p$.e && (_p$.e = _$setProp(_el$15, "fg", _v$6, _p$.e));
914
+ _v$7 !== _p$.t && (_p$.t = _$setProp(_el$21, "fg", _v$7, _p$.t));
915
+ return _p$;
916
+ }, {
917
+ e: undefined,
918
+ t: undefined
919
+ });
920
+ return _el$14;
921
+ })();
922
+ }
923
+ var tui = async (api) => {
924
+ const [revision, setRevision] = createSignal(0);
925
+ const tracker = createTracker(api, () => setRevision((value) => value + 1));
926
+ api.lifecycle.onDispose(() => tracker.dispose());
927
+ api.slots.register({
928
+ slots: {
929
+ sidebar_footer(_context, props) {
930
+ return _$createComponent(ModelPanelView, {
931
+ api,
932
+ get sessionID() {
933
+ return props.session_id;
934
+ },
935
+ tracker,
936
+ revision
937
+ });
938
+ }
939
+ }
940
+ });
941
+ };
942
+ var plugin = {
943
+ id: "opencode-tui-stats",
944
+ tui
945
+ };
946
+ var tui_default = plugin;
947
+ export {
948
+ tui_default as default
949
+ };