@ymys4ever/opencode-throughput-v2 4.0.1 → 4.0.2

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,663 @@
1
+ // src/tui.tsx
2
+ import { Plugin } from "@opencode/plugin/tui";
3
+
4
+ // src/core/step-tracker.ts
5
+ function getModelKey(providerID, modelID) {
6
+ return `${providerID}/${modelID}`;
7
+ }
8
+ var ZERO_TOKENS = {
9
+ input: 0,
10
+ output: 0,
11
+ reasoning: 0,
12
+ cache: { read: 0, write: 0 }
13
+ };
14
+ function buildLogEntry(state, eventAt, terminal, recordedAt) {
15
+ const tokens = terminal.tokens ?? ZERO_TOKENS;
16
+ const latencyMs = eventAt - state.started;
17
+ const ttftMs = state.firstOutput != null ? state.firstOutput - state.started : null;
18
+ const genEnd = state.streamEnded ?? eventAt;
19
+ const genTimeMs = state.firstOutput != null ? genEnd - state.firstOutput : eventAt - state.started;
20
+ const tps = tokens.output > 0 && genTimeMs > 0 ? tokens.output / genTimeMs * 1000 : null;
21
+ return {
22
+ ts: new Date(recordedAt).toISOString(),
23
+ model: state.modelKey,
24
+ providerID: state.providerID,
25
+ modelID: state.modelID,
26
+ sessionID: terminal.sessionID,
27
+ messageID: terminal.assistantMessageID,
28
+ ttft_ms: ttftMs,
29
+ tps,
30
+ latency_ms: latencyMs,
31
+ inputTokens: tokens.input,
32
+ outputTokens: tokens.output,
33
+ reasoningTokens: tokens.reasoning,
34
+ cacheReadTokens: tokens.cache.read,
35
+ cacheWriteTokens: tokens.cache.write,
36
+ cost: terminal.cost ?? 0,
37
+ finish: terminal.finish ?? "error"
38
+ };
39
+ }
40
+ function buildAbortedEntry(state, assistantMessageID, now) {
41
+ return buildLogEntry(state, now, { sessionID: state.sessionID, assistantMessageID, finish: "aborted" }, now);
42
+ }
43
+
44
+ class StepTracker {
45
+ active = new Map;
46
+ parked = new Map;
47
+ activeTtlMs;
48
+ constructor(options = {}) {
49
+ const ttl = options.activeTtlMs ?? 600000;
50
+ this.activeTtlMs = ttl > 0 ? ttl : 0;
51
+ }
52
+ start(data) {
53
+ this.active.set(data.assistantMessageID, {
54
+ sessionID: data.sessionID,
55
+ started: data.started,
56
+ providerID: data.model.providerID,
57
+ modelID: data.model.id,
58
+ modelKey: getModelKey(data.model.providerID, data.model.id),
59
+ firstOutput: null,
60
+ streamEnded: null
61
+ });
62
+ }
63
+ markFirstOutput(assistantMessageID, created) {
64
+ const state = this.lookup(assistantMessageID);
65
+ if (state && state.firstOutput == null)
66
+ state.firstOutput = created;
67
+ }
68
+ markStreamed(assistantMessageID, created) {
69
+ const state = this.lookup(assistantMessageID);
70
+ if (state && state.streamEnded == null)
71
+ state.streamEnded = created;
72
+ }
73
+ settle(assistantMessageID, eventAt, terminal, recordedAt) {
74
+ const activeState = this.active.get(assistantMessageID);
75
+ if (activeState) {
76
+ this.active.delete(assistantMessageID);
77
+ return buildLogEntry(activeState, eventAt, terminal, recordedAt);
78
+ }
79
+ const parked = this.parked.get(assistantMessageID);
80
+ if (parked) {
81
+ this.parked.delete(assistantMessageID);
82
+ return buildLogEntry(parked.state, eventAt, terminal, recordedAt);
83
+ }
84
+ return null;
85
+ }
86
+ parkSession(sessionID, now, ttlMs) {
87
+ const deadline = now + ttlMs;
88
+ for (const [id, state] of this.active) {
89
+ if (state.sessionID === sessionID) {
90
+ this.parked.set(id, { state, deadline });
91
+ this.active.delete(id);
92
+ }
93
+ }
94
+ }
95
+ collectExpired(now) {
96
+ const expired = [];
97
+ for (const [id, parked] of this.parked) {
98
+ if (parked.deadline <= now) {
99
+ expired.push({ assistantMessageID: id, state: parked.state });
100
+ this.parked.delete(id);
101
+ }
102
+ }
103
+ if (this.activeTtlMs > 0) {
104
+ for (const [id, state] of this.active) {
105
+ if (now - state.started >= this.activeTtlMs) {
106
+ expired.push({ assistantMessageID: id, state });
107
+ this.active.delete(id);
108
+ }
109
+ }
110
+ }
111
+ return expired;
112
+ }
113
+ nextDeadline() {
114
+ let next = null;
115
+ const consider = (deadline) => {
116
+ if (next == null || deadline < next)
117
+ next = deadline;
118
+ };
119
+ for (const { deadline } of this.parked.values())
120
+ consider(deadline);
121
+ if (this.activeTtlMs > 0) {
122
+ for (const { started } of this.active.values())
123
+ consider(started + this.activeTtlMs);
124
+ }
125
+ return next;
126
+ }
127
+ get size() {
128
+ return this.active.size + this.parked.size;
129
+ }
130
+ lookup(assistantMessageID) {
131
+ return this.active.get(assistantMessageID) ?? this.parked.get(assistantMessageID)?.state;
132
+ }
133
+ }
134
+
135
+ // src/core/events.ts
136
+ var SUBSCRIBED_EVENT_TYPES = [
137
+ "session.step.started",
138
+ "session.text.started",
139
+ "session.reasoning.started",
140
+ "session.step.streamed",
141
+ "session.step.ended",
142
+ "session.step.failed",
143
+ "session.idle"
144
+ ];
145
+ function normalizeEvent(raw) {
146
+ if (typeof raw === "object" && raw != null && "type" in raw) {
147
+ const type = raw.type;
148
+ if (typeof type === "string" && SUBSCRIBED_EVENT_TYPES.includes(type)) {
149
+ return { ok: true, event: raw };
150
+ }
151
+ }
152
+ return { ok: false, reason: "unknown" };
153
+ }
154
+ function applyThroughputEvent(tracker, ev, deps) {
155
+ switch (ev.type) {
156
+ case "session.step.started":
157
+ tracker.start(ev.data);
158
+ return null;
159
+ case "session.text.started":
160
+ case "session.reasoning.started":
161
+ tracker.markFirstOutput(ev.data.assistantMessageID, ev.created);
162
+ return null;
163
+ case "session.step.streamed":
164
+ tracker.markStreamed(ev.data.assistantMessageID, ev.created);
165
+ return null;
166
+ case "session.step.ended":
167
+ case "session.step.failed":
168
+ return tracker.settle(ev.data.assistantMessageID, ev.created, ev.data, deps.recordedAt);
169
+ case "session.idle":
170
+ tracker.parkSession(ev.data.sessionID, ev.created, deps.abortTtlMs);
171
+ return null;
172
+ }
173
+ return null;
174
+ }
175
+
176
+ // src/runtime/pipeline.ts
177
+ function makeSafeOnError(onError) {
178
+ return (e) => {
179
+ try {
180
+ onError(e);
181
+ } catch (reporterError) {
182
+ console.error(reporterError);
183
+ }
184
+ };
185
+ }
186
+ function createPipeline(deps) {
187
+ const timers = deps.timers ?? {
188
+ set: (cb, ms) => setTimeout(cb, ms),
189
+ clear: (h) => clearTimeout(h)
190
+ };
191
+ const safeOnError = makeSafeOnError(deps.onError);
192
+ const { tracker, now, abortTtlMs, onEntry } = deps;
193
+ let flushTimer = null;
194
+ let disposed = false;
195
+ function drainExpired() {
196
+ for (const { assistantMessageID, state } of tracker.collectExpired(now())) {
197
+ try {
198
+ onEntry(buildAbortedEntry(state, assistantMessageID, now()));
199
+ } catch (e) {
200
+ safeOnError(e);
201
+ }
202
+ }
203
+ }
204
+ function rearm() {
205
+ if (flushTimer != null)
206
+ timers.clear(flushTimer);
207
+ flushTimer = null;
208
+ if (disposed)
209
+ return;
210
+ const next = tracker.nextDeadline();
211
+ if (next == null)
212
+ return;
213
+ flushTimer = timers.set(() => {
214
+ flushTimer = null;
215
+ if (disposed)
216
+ return;
217
+ try {
218
+ drainExpired();
219
+ } catch (e) {
220
+ safeOnError(e);
221
+ } finally {
222
+ rearm();
223
+ }
224
+ }, Math.max(0, next - now()));
225
+ }
226
+ function dispatch(raw) {
227
+ if (disposed)
228
+ return;
229
+ let owned;
230
+ try {
231
+ owned = deps.owns(raw);
232
+ } catch (e) {
233
+ safeOnError(e);
234
+ return;
235
+ }
236
+ if (!owned)
237
+ return;
238
+ const n = normalizeEvent(raw);
239
+ if (!n.ok && n.reason === "malformed") {
240
+ safeOnError(new Error("throughput: malformed event rejected"));
241
+ return;
242
+ }
243
+ try {
244
+ if (n.ok) {
245
+ const recordedAt = now();
246
+ const entry = applyThroughputEvent(tracker, n.event, { recordedAt, abortTtlMs });
247
+ if (entry)
248
+ onEntry(entry);
249
+ }
250
+ } catch (e) {
251
+ safeOnError(e);
252
+ } finally {
253
+ drainExpired();
254
+ rearm();
255
+ }
256
+ }
257
+ function dispose() {
258
+ if (disposed)
259
+ return;
260
+ disposed = true;
261
+ if (flushTimer != null) {
262
+ timers.clear(flushTimer);
263
+ flushTimer = null;
264
+ }
265
+ }
266
+ return { dispatch, dispose };
267
+ }
268
+
269
+ // src/core/ownership.ts
270
+ function makeOwns(selfDir) {
271
+ return (ev) => {
272
+ const dir = ev.location?.directory;
273
+ return dir == null || selfDir == null || dir === selfDir;
274
+ };
275
+ }
276
+
277
+ // src/core/stats.ts
278
+ function aggregateAt(entries, now) {
279
+ const groups = new Map;
280
+ for (const entry of entries) {
281
+ let g = groups.get(entry.model);
282
+ if (!g) {
283
+ g = {
284
+ count: 0,
285
+ ttft: newMetricFold(),
286
+ tps: newMetricFold(),
287
+ latency: newMetricFold(),
288
+ totalInputTokens: 0,
289
+ totalOutputTokens: 0,
290
+ totalReasoningTokens: 0,
291
+ totalCacheReadTokens: 0,
292
+ totalCacheWriteTokens: 0,
293
+ totalCost: 0
294
+ };
295
+ groups.set(entry.model, g);
296
+ }
297
+ g.count++;
298
+ if (entry.ttft_ms != null)
299
+ foldMetric(g.ttft, entry.ttft_ms);
300
+ if (entry.tps != null)
301
+ foldMetric(g.tps, entry.tps);
302
+ if (entry.latency_ms != null)
303
+ foldMetric(g.latency, entry.latency_ms);
304
+ g.totalInputTokens += entry.inputTokens;
305
+ g.totalOutputTokens += entry.outputTokens;
306
+ g.totalReasoningTokens += entry.reasoningTokens;
307
+ g.totalCacheReadTokens += entry.cacheReadTokens;
308
+ g.totalCacheWriteTokens += entry.cacheWriteTokens;
309
+ g.totalCost += entry.cost;
310
+ }
311
+ const result = new Map;
312
+ for (const [model, g] of groups) {
313
+ result.set(model, {
314
+ model,
315
+ count: g.count,
316
+ avgTTFT: metricAvg(g.ttft),
317
+ minTTFT: metricMin(g.ttft),
318
+ maxTTFT: metricMax(g.ttft),
319
+ avgTPS: metricAvg(g.tps),
320
+ maxTPS: metricMax(g.tps),
321
+ avgLatency: metricAvg(g.latency),
322
+ minLatency: metricMin(g.latency),
323
+ maxLatency: metricMax(g.latency),
324
+ totalInputTokens: g.totalInputTokens,
325
+ totalOutputTokens: g.totalOutputTokens,
326
+ totalReasoningTokens: g.totalReasoningTokens,
327
+ totalCacheReadTokens: g.totalCacheReadTokens,
328
+ totalCacheWriteTokens: g.totalCacheWriteTokens,
329
+ totalCost: g.totalCost,
330
+ lastUpdated: now
331
+ });
332
+ }
333
+ return result;
334
+ }
335
+ function newMetricFold() {
336
+ return { sum: 0, count: 0, min: Infinity, max: -Infinity };
337
+ }
338
+ function foldMetric(m, v) {
339
+ m.sum += v;
340
+ m.count++;
341
+ if (v < m.min)
342
+ m.min = v;
343
+ if (v > m.max)
344
+ m.max = v;
345
+ }
346
+ function metricAvg(m) {
347
+ return m.count > 0 ? m.sum / m.count : null;
348
+ }
349
+ function metricMin(m) {
350
+ return m.count > 0 ? m.min : null;
351
+ }
352
+ function metricMax(m) {
353
+ return m.count > 0 ? m.max : null;
354
+ }
355
+
356
+ // src/format/entry.ts
357
+ function formatMs(ms) {
358
+ if (ms == null)
359
+ return "N/A";
360
+ if (ms >= 60000)
361
+ return (ms / 60000).toFixed(1) + "m";
362
+ if (ms >= 1000)
363
+ return (ms / 1000).toFixed(1) + "s";
364
+ return ms.toFixed(0) + "ms";
365
+ }
366
+ function formatTokenCount(n) {
367
+ if (n >= 1e6)
368
+ return (n / 1e6).toFixed(2) + "M";
369
+ if (n >= 1000)
370
+ return (n / 1000).toFixed(1) + "k";
371
+ return n.toFixed(n >= 100 ? 0 : 1);
372
+ }
373
+ function formatNum(n) {
374
+ if (n == null)
375
+ return "N/A";
376
+ return formatTokenCount(n);
377
+ }
378
+ function truncate(s, max) {
379
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
380
+ }
381
+ function buildLogMsg(entry) {
382
+ const parts = [
383
+ truncate(entry.modelID, 28),
384
+ formatMs(entry.ttft_ms) + " TTFT",
385
+ formatNum(entry.tps) + " tok/s",
386
+ formatMs(entry.latency_ms),
387
+ "↑" + formatNum(entry.inputTokens) + " ↓" + formatNum(entry.outputTokens)
388
+ ];
389
+ return parts.join(" | ");
390
+ }
391
+
392
+ // src/ui/sidebar.ts
393
+ function abbreviateModel(modelID) {
394
+ const tokens = modelID.split(/[^a-zA-Z0-9]+/).filter(Boolean);
395
+ const versionIndex = tokens.findIndex((token) => /\d/.test(token));
396
+ const provider = (tokens[0]?.[0] ?? "?").toLowerCase();
397
+ if (versionIndex < 0)
398
+ return (provider + (tokens[1]?.[0] ?? " ").toLowerCase() + " ").slice(0, 3).padEnd(3, " ");
399
+ const version = tokens[versionIndex].match(/\d/)?.[0] ?? " ";
400
+ const suffix = tokens.filter((token, index) => index !== 0 && index !== versionIndex && !/^\d+(?:\.\d+)*$/.test(token)).at(-1);
401
+ return `${provider}${version}${(suffix?.[0] ?? " ").toLowerCase()}`;
402
+ }
403
+ var COL_ID = 3;
404
+ var COL_TIME = 4;
405
+ var COL_TOKEN = 5;
406
+ function buildDisplayIds(modelIDs) {
407
+ const taken = new Set;
408
+ const ids = new Map;
409
+ for (const modelID of modelIDs) {
410
+ if (ids.has(modelID))
411
+ continue;
412
+ const candidates = [abbreviateModel(modelID), modelID.slice(0, COL_ID + 1), modelID.slice(0, COL_TOKEN)];
413
+ let chosen = candidates[candidates.length - 1];
414
+ for (const candidate of candidates) {
415
+ if (!taken.has(candidate)) {
416
+ chosen = candidate;
417
+ break;
418
+ }
419
+ }
420
+ taken.add(chosen);
421
+ ids.set(modelID, chosen);
422
+ }
423
+ return ids;
424
+ }
425
+ function formatSidebarSeconds(ms) {
426
+ if (ms == null)
427
+ return " N/A";
428
+ return fitColumn4(ms / 1000);
429
+ }
430
+ function formatSidebarMetric(value) {
431
+ if (value == null)
432
+ return " N/A";
433
+ return fitColumn4(value);
434
+ }
435
+ function fitColumn4(value) {
436
+ const s = value.toFixed(1);
437
+ return s.length > COL_TIME ? value.toFixed(0) : s;
438
+ }
439
+ function formatSidebarTokens(value) {
440
+ value = Math.max(0, Math.round(value));
441
+ if (value < 1000)
442
+ return value.toFixed(1);
443
+ if (value < 99950)
444
+ return (value / 1000).toFixed(1) + "k";
445
+ if (value < 999500)
446
+ return (value / 1000).toFixed(0) + "k";
447
+ if (value < 99950000)
448
+ return (value / 1e6).toFixed(1) + "m";
449
+ if (value < 999500000)
450
+ return (value / 1e6).toFixed(0) + "m";
451
+ return "999m";
452
+ }
453
+ function formatSidebarRow(displayID, ttft, tps, latency, input, output) {
454
+ return ` ${displayID.padEnd(COL_ID)} ${formatSidebarSeconds(ttft).padStart(COL_TIME)} ${formatSidebarMetric(tps).padStart(COL_TIME)} ${formatSidebarSeconds(latency).padStart(COL_TIME)} ${formatSidebarTokens(input).padStart(COL_TOKEN)} ${formatSidebarTokens(output).padStart(COL_TOKEN)}`;
455
+ }
456
+ function formatSidebarHeader() {
457
+ return ` ${"ID".padEnd(COL_ID)} ${"TTFT".padStart(COL_TIME)} ${"TPS".padStart(COL_TIME)} ${"Lat".padStart(COL_TIME)} ${"↑In".padStart(COL_TOKEN)} ${"↓Out".padStart(COL_TOKEN)}`;
458
+ }
459
+
460
+ // src/tui/controller.ts
461
+ var MAX_RECENT = 10;
462
+ var BUFFER_CAP = 200;
463
+ function createThroughputController(context, options = {}) {
464
+ const onError = options.onError ?? ((e) => console.error(e));
465
+ const now = options.now ?? Date.now;
466
+ const abortTtlMs = options.abortTtlMs ?? 5000;
467
+ const tracker = new StepTracker({ activeTtlMs: options.activeTtlMs });
468
+ const buffers = new Map;
469
+ const owns = makeOwns(context.location?.directory);
470
+ const initial = { open: true, recent: [], rows: [] };
471
+ let fallback = { ...initial };
472
+ const store = context.storage?.memory("opencode-throughput:tui", { initial });
473
+ if (store) {
474
+ try {
475
+ store[1]((draft) => {
476
+ draft.open = true;
477
+ draft.recent = [];
478
+ draft.rows = [];
479
+ });
480
+ } catch (e) {
481
+ onError(e);
482
+ }
483
+ }
484
+ const read = () => store ? store[0] : fallback;
485
+ const mutate = (fn) => {
486
+ if (store)
487
+ store[1](fn);
488
+ else
489
+ fn(fallback);
490
+ context.renderer?.requestRender();
491
+ };
492
+ function recomputeRows() {
493
+ const modelIDs = new Set;
494
+ for (const entries of buffers.values())
495
+ modelIDs.add(entries[entries.length - 1].modelID);
496
+ const displayIds = buildDisplayIds([...modelIDs]);
497
+ const rows = [];
498
+ for (const [modelKey, entries] of buffers) {
499
+ const stats = aggregateAt(entries, now()).get(modelKey);
500
+ if (!stats)
501
+ continue;
502
+ const modelID = entries[entries.length - 1].modelID;
503
+ rows.push({ stats, modelID, displayID: displayIds.get(modelID) });
504
+ }
505
+ return rows;
506
+ }
507
+ function showToast(entry) {
508
+ context.ui.toast.show({
509
+ title: "Throughput",
510
+ message: buildLogMsg(entry),
511
+ variant: entry.finish === "stop" || entry.finish === "aborted" ? "info" : "warning",
512
+ duration: 4000
513
+ });
514
+ }
515
+ function record(entry) {
516
+ const buffer = buffers.get(entry.model) ?? [];
517
+ buffer.push(entry);
518
+ if (buffer.length > BUFFER_CAP)
519
+ buffer.splice(0, buffer.length - BUFFER_CAP);
520
+ buffers.set(entry.model, buffer);
521
+ const rows = recomputeRows();
522
+ mutate((draft) => {
523
+ const next = [...draft.recent, entry];
524
+ draft.recent = next.length > MAX_RECENT ? next.slice(-MAX_RECENT) : next;
525
+ draft.rows = rows;
526
+ });
527
+ showToast(entry);
528
+ }
529
+ const pipeline = createPipeline({
530
+ tracker,
531
+ owns,
532
+ now,
533
+ abortTtlMs,
534
+ onEntry: record,
535
+ onError,
536
+ timers: {
537
+ set: (cb, ms) => setTimeout(cb, ms),
538
+ clear: (h) => clearTimeout(h)
539
+ }
540
+ });
541
+ const onSessionEvent = (ev) => pipeline.dispatch(ev);
542
+ const stops = SUBSCRIBED_EVENT_TYPES.map((t) => context.data.on(t, onSessionEvent));
543
+ function unsubscribe() {
544
+ pipeline.dispose();
545
+ for (const off of stops)
546
+ off();
547
+ }
548
+ return {
549
+ recent: () => read().recent,
550
+ statsRows: () => read().rows,
551
+ open: () => read().open,
552
+ toggleOpen: () => mutate((draft) => {
553
+ draft.open = !draft.open;
554
+ }),
555
+ unsubscribe
556
+ };
557
+ }
558
+
559
+ // src/ui/throughput-widget.tsx
560
+ import { jsxDEV } from "@opentui/solid/jsx-dev-runtime";
561
+ function ThroughputWidget(props) {
562
+ const fg = () => props.theme.text?.base;
563
+ const muted = () => props.theme.text?.muted;
564
+ const totalCount = () => props.stats().reduce((sum, row) => sum + row.stats.count, 0);
565
+ const displayIDFor = (modelID) => {
566
+ for (const row of props.stats()) {
567
+ if (row.modelID === modelID)
568
+ return row.displayID;
569
+ }
570
+ return abbreviateModel(modelID);
571
+ };
572
+ return /* @__PURE__ */ jsxDEV("box", {
573
+ children: [
574
+ /* @__PURE__ */ jsxDEV("box", {
575
+ flexDirection: "row",
576
+ gap: 1,
577
+ onMouseDown: () => totalCount() > 0 && props.onToggle(),
578
+ children: [
579
+ totalCount() > 0 ? /* @__PURE__ */ jsxDEV("text", {
580
+ fg: fg(),
581
+ children: props.open() ? "▼" : "▶"
582
+ }, undefined, false, undefined, this) : null,
583
+ /* @__PURE__ */ jsxDEV("text", {
584
+ fg: fg(),
585
+ children: /* @__PURE__ */ jsxDEV("b", {
586
+ children: "Throughput"
587
+ }, undefined, false, undefined, this)
588
+ }, undefined, false, undefined, this),
589
+ totalCount() > 0 ? /* @__PURE__ */ jsxDEV("text", {
590
+ fg: muted(),
591
+ children: totalCount() + " reqs"
592
+ }, undefined, false, undefined, this) : null
593
+ ]
594
+ }, undefined, true, undefined, this),
595
+ totalCount() === 0 ? /* @__PURE__ */ jsxDEV("text", {
596
+ fg: muted(),
597
+ children: " Waiting for requests..."
598
+ }, undefined, false, undefined, this) : null,
599
+ props.open() && totalCount() > 0 ? /* @__PURE__ */ jsxDEV("box", {
600
+ flexDirection: "column",
601
+ children: [
602
+ /* @__PURE__ */ jsxDEV("text", {
603
+ fg: muted(),
604
+ children: formatSidebarHeader()
605
+ }, undefined, false, undefined, this),
606
+ props.stats().map((row) => /* @__PURE__ */ jsxDEV("text", {
607
+ fg: muted(),
608
+ children: formatSidebarRow(row.displayID, row.stats.avgTTFT, row.stats.avgTPS, row.stats.avgLatency, row.stats.totalInputTokens, row.stats.totalOutputTokens)
609
+ }, undefined, false, undefined, this)),
610
+ props.recent().length > 0 ? /* @__PURE__ */ jsxDEV("box", {
611
+ flexDirection: "column",
612
+ children: [
613
+ /* @__PURE__ */ jsxDEV("text", {
614
+ fg: fg(),
615
+ children: [
616
+ " ",
617
+ /* @__PURE__ */ jsxDEV("b", {
618
+ children: "Recent"
619
+ }, undefined, false, undefined, this)
620
+ ]
621
+ }, undefined, true, undefined, this),
622
+ props.recent().map((entry) => /* @__PURE__ */ jsxDEV("text", {
623
+ fg: muted(),
624
+ children: formatSidebarRow(displayIDFor(entry.modelID), entry.ttft_ms, entry.tps, entry.latency_ms, entry.inputTokens, entry.outputTokens)
625
+ }, undefined, false, undefined, this))
626
+ ]
627
+ }, undefined, true, undefined, this) : null
628
+ ]
629
+ }, undefined, true, undefined, this) : null
630
+ ]
631
+ }, undefined, true, undefined, this);
632
+ }
633
+
634
+ // src/tui.tsx
635
+ function createThroughputTuiPlugin(options = {}) {
636
+ return Plugin.define({
637
+ id: "opencode-throughput",
638
+ setup(context) {
639
+ const controller = createThroughputController(context, options);
640
+ const stopSlot = context.ui.slot({
641
+ append: "sidebar.content",
642
+ render: () => ThroughputWidget({
643
+ theme: context.theme,
644
+ recent: controller.recent,
645
+ stats: controller.statsRows,
646
+ open: controller.open,
647
+ onToggle: controller.toggleOpen
648
+ })
649
+ });
650
+ return () => {
651
+ stopSlot();
652
+ controller.unsubscribe();
653
+ };
654
+ }
655
+ });
656
+ }
657
+ var ThroughputTuiPlugin = createThroughputTuiPlugin();
658
+ var tui_default = ThroughputTuiPlugin;
659
+ export {
660
+ tui_default as default,
661
+ createThroughputTuiPlugin,
662
+ ThroughputTuiPlugin
663
+ };
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@ymys4ever/opencode-throughput-v2",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "description": "Real-time LLM performance monitoring plugin for OpenCode V2. Tracks TTFT, TPS, latency, token usage and cost per model with toast notifications and JSONL logging.",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
7
  "exports": {
8
- ".": "./src/index.ts",
9
- "./tui": "./src/tui.tsx"
8
+ ".": "./dist/index.js",
9
+ "./tui": "./dist/tui.js"
10
10
  },
11
11
  "files": [
12
- "src"
12
+ "dist"
13
13
  ],
14
14
  "scripts": {
15
- "build": "bun build src/index.ts --outdir dist --target node --external @opencode/plugin --external effect",
15
+ "build": "bun build src/index.ts src/tui.tsx --outdir dist --target node --external @opencode/plugin --external \"@opencode/plugin/*\" --external effect --external @opentui/core --external \"@opentui/*\" --external solid-js",
16
16
  "prepublishOnly": "bun run build",
17
17
  "typecheck": "tsc --noEmit"
18
18
  },