oc-metricboard 0.1.21 → 0.1.22
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/components/SidebarMetrics.js +227 -69
- package/dist/components/StatRow.js +25 -18
- package/dist/components/ui-element.js +40 -0
- package/dist/tui-v2.js +15 -5
- package/dist/tui.js +21 -2
- package/package.json +6 -1
- package/src/components/SidebarMetrics.tsx +396 -379
- package/src/components/StatRow.tsx +87 -83
- package/src/components/ui-element.ts +44 -0
- package/src/tui-v2.tsx +15 -14
- package/src/tui.tsx +76 -68
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/** @jsxImportSource @opentui/solid */
|
|
3
|
-
/** @jsxRuntime automatic */
|
|
4
|
-
import { createMemo, createSignal, onCleanup } from "solid-js";
|
|
1
|
+
import { createMemo, createSignal } from "solid-js";
|
|
5
2
|
import { formatTokens, formatDuration, formatElapsed, formatCacheRead, } from "../metrics";
|
|
6
3
|
import { StatRow } from "./StatRow";
|
|
4
|
+
import { element } from "./ui-element";
|
|
7
5
|
// Upper bound on model-breakdown rows rendered in the sidebar. This runtime's
|
|
8
|
-
//
|
|
6
|
+
// tree is not reactive, so rows are pre-rendered once and toggled via
|
|
9
7
|
// registerSync; more sub-agents than this simply scroll off.
|
|
10
8
|
const MAX_MODEL_ROWS = 8;
|
|
11
9
|
export function SidebarMetrics(props) {
|
|
@@ -44,7 +42,13 @@ export function SidebarMetrics(props) {
|
|
|
44
42
|
props.requestRender?.();
|
|
45
43
|
});
|
|
46
44
|
};
|
|
47
|
-
onCleanup
|
|
45
|
+
// Explicit disposal replacing the old onCleanup: the V2 Node loader builds
|
|
46
|
+
// this tree outside a dispose-owned JSX scope, so onCleanup was wired to a
|
|
47
|
+
// scope we do not control. The setup cleanup path (tui.tsx onDispose /
|
|
48
|
+
// tui-v2.tsx setup return) calls this instead.
|
|
49
|
+
const dispose = () => {
|
|
50
|
+
if (disposed)
|
|
51
|
+
return;
|
|
48
52
|
disposed = true;
|
|
49
53
|
refreshQueued = false;
|
|
50
54
|
rowSyncs.clear();
|
|
@@ -52,7 +56,7 @@ export function SidebarMetrics(props) {
|
|
|
52
56
|
clearInterval(interval);
|
|
53
57
|
unsub();
|
|
54
58
|
unsubController();
|
|
55
|
-
}
|
|
59
|
+
};
|
|
56
60
|
interval = setInterval(bump, props.refreshIntervalMs);
|
|
57
61
|
unsub = props.collector.subscribe(bump);
|
|
58
62
|
unsubController = props.controller.subscribe(bump);
|
|
@@ -74,9 +78,9 @@ export function SidebarMetrics(props) {
|
|
|
74
78
|
return props.controller.collapsed();
|
|
75
79
|
});
|
|
76
80
|
const headerLabel = () => props.controller.prefs().section.label;
|
|
77
|
-
// This runtime's
|
|
78
|
-
// mount, so the ▶/▼ arrow would freeze on its initial value. Two
|
|
79
|
-
// bold nodes (collapsed/expanded) are pre-rendered and toggled
|
|
81
|
+
// This runtime's tree is non-reactive: the header <text> nodes are built
|
|
82
|
+
// once at mount, so the ▶/▼ arrow would freeze on its initial value. Two
|
|
83
|
+
// static bold nodes (collapsed/expanded) are pre-rendered and toggled
|
|
80
84
|
// imperatively through registerSync — the same mechanism as the rows.
|
|
81
85
|
// Hidden node must be zero-sized in BOTH axes: visible=false only
|
|
82
86
|
// suppresses painting, while yoga still reserves its box, which would
|
|
@@ -108,8 +112,9 @@ export function SidebarMetrics(props) {
|
|
|
108
112
|
catch { /* cosmetic — ignore layout failures */ }
|
|
109
113
|
}
|
|
110
114
|
};
|
|
111
|
-
|
|
112
|
-
onCleanup
|
|
115
|
+
// Unregistered wholesale by dispose() via rowSyncs.clear() (the old
|
|
116
|
+
// per-sync onCleanup unregister went with the JSX scope).
|
|
117
|
+
registerRowSync(syncHeader);
|
|
113
118
|
const attachCollapsedHeaderNode = (node) => {
|
|
114
119
|
collapsedHeaderNode = node;
|
|
115
120
|
syncHeader();
|
|
@@ -159,61 +164,214 @@ export function SidebarMetrics(props) {
|
|
|
159
164
|
const sessionValue = () => {
|
|
160
165
|
return formatElapsed(props.collector.getSessionElapsedMs(props.sessionID, currentScope(), performance.now()));
|
|
161
166
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
167
|
+
// --- tree (same hierarchy/spacing/order as the old JSX) ------------------
|
|
168
|
+
const headerBox = element("box", {
|
|
169
|
+
width: "100%",
|
|
170
|
+
flexDirection: "row",
|
|
171
|
+
alignItems: "center",
|
|
172
|
+
ref: attachBoxToggle,
|
|
173
|
+
}, [
|
|
174
|
+
element("text", {
|
|
175
|
+
ref: attachCollapsedHeaderNode,
|
|
176
|
+
fg: props.theme.text,
|
|
177
|
+
visible: false,
|
|
178
|
+
}, [
|
|
179
|
+
element("b", {}, ["▶ ", headerLabel()]),
|
|
180
|
+
]),
|
|
181
|
+
element("text", {
|
|
182
|
+
ref: attachExpandedHeaderNode,
|
|
183
|
+
fg: props.theme.text,
|
|
184
|
+
}, [
|
|
185
|
+
element("b", {}, ["▼ ", headerLabel()]),
|
|
186
|
+
]),
|
|
187
|
+
]);
|
|
188
|
+
const rows = [];
|
|
189
|
+
rows.push(StatRow({
|
|
190
|
+
theme: props.theme,
|
|
191
|
+
label: "Status",
|
|
192
|
+
value: "No active request",
|
|
193
|
+
dim: true,
|
|
194
|
+
icon: "○",
|
|
195
|
+
visible: expandedIdle,
|
|
196
|
+
registerSync: registerRowSync,
|
|
197
|
+
}));
|
|
198
|
+
if (rowVisible("speed")) {
|
|
199
|
+
rows.push(StatRow({
|
|
200
|
+
theme: props.theme,
|
|
201
|
+
label: "TPS",
|
|
202
|
+
value: speedValue,
|
|
203
|
+
accent: true,
|
|
204
|
+
icon: "⚡",
|
|
205
|
+
registerSync: registerRowSync,
|
|
206
|
+
visible: expandedActive,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
if (rowVisible("elapsed")) {
|
|
210
|
+
rows.push(StatRow({
|
|
211
|
+
theme: props.theme,
|
|
212
|
+
label: "Elapsed",
|
|
213
|
+
value: elapsedValue,
|
|
214
|
+
icon: "▹",
|
|
215
|
+
registerSync: registerRowSync,
|
|
216
|
+
visible: expandedActive,
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
if (rowVisible("ttft")) {
|
|
220
|
+
rows.push(StatRow({
|
|
221
|
+
theme: props.theme,
|
|
222
|
+
label: "TTFT",
|
|
223
|
+
value: ttftValue,
|
|
224
|
+
icon: "⏱",
|
|
225
|
+
registerSync: registerRowSync,
|
|
226
|
+
visible: expandedActive,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
if (rowVisible("input") || rowVisible("output")) {
|
|
230
|
+
rows.push(StatRow({
|
|
231
|
+
theme: props.theme,
|
|
232
|
+
label: "Tokens",
|
|
233
|
+
value: tokenValue,
|
|
234
|
+
registerSync: registerRowSync,
|
|
235
|
+
visible: expandedActive,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
if (rowVisible("cache")) {
|
|
239
|
+
rows.push(StatRow({
|
|
240
|
+
theme: props.theme,
|
|
241
|
+
label: "Cache",
|
|
242
|
+
value: cacheValue,
|
|
243
|
+
dim: true,
|
|
244
|
+
icon: "○",
|
|
245
|
+
registerSync: registerRowSync,
|
|
246
|
+
visible: expandedActive,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
if (rowVisible("session")) {
|
|
250
|
+
rows.push(StatRow({
|
|
251
|
+
theme: props.theme,
|
|
252
|
+
label: "Session",
|
|
253
|
+
value: sessionValue,
|
|
254
|
+
icon: "◷",
|
|
255
|
+
registerSync: registerRowSync,
|
|
256
|
+
visible: expandedActive,
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
// Model breakdown rows for tree scope. This runtime's tree is NOT
|
|
260
|
+
// reactive — it is built once at mount, so inline .map() evaluates once:
|
|
261
|
+
// rows for sub-agents spawned later would never appear. Fix: pre-render a
|
|
262
|
+
// fixed number of rows that pull their data live from currentAggregate()
|
|
263
|
+
// on every registerSync tick, exactly like the static Speed/TTFT rows.
|
|
264
|
+
if (rowVisible("modelBreakdown")) {
|
|
265
|
+
for (let i = 0; i < MAX_MODEL_ROWS; i++) {
|
|
266
|
+
// Line 1: short model name + ×N session count.
|
|
267
|
+
const modelLabel = () => {
|
|
268
|
+
const m = currentAggregate()?.modelBreakdown[i];
|
|
269
|
+
if (!m)
|
|
270
|
+
return "";
|
|
271
|
+
const count = m.sessionCount > 1 ? ` ×${m.sessionCount}` : "";
|
|
272
|
+
return `${m.modelID}${count}`;
|
|
273
|
+
};
|
|
274
|
+
// Line 2: metrics for that model group. Compact
|
|
275
|
+
// format (no spaces after icons, no unit suffixes,
|
|
276
|
+
// single-space separators) so the data line fits the
|
|
277
|
+
// narrow sidebar on ONE line without right-edge
|
|
278
|
+
// truncation.
|
|
279
|
+
const modelValue = () => {
|
|
280
|
+
const m = currentAggregate()?.modelBreakdown[i];
|
|
281
|
+
if (!m)
|
|
282
|
+
return "";
|
|
283
|
+
const parts = [];
|
|
284
|
+
if (rowVisible("speed"))
|
|
285
|
+
parts.push(`⚡${m.liveTps !== null ? m.liveTps.toFixed(1) : "—"}`);
|
|
286
|
+
if (rowVisible("ttft"))
|
|
287
|
+
parts.push(`⏱${m.ttft !== null ? " " + formatDuration(m.ttft) : " --"}`);
|
|
288
|
+
if (rowVisible("input"))
|
|
289
|
+
parts.push(`↓${formatTokens(m.inputTokens)}`);
|
|
290
|
+
if (rowVisible("output"))
|
|
291
|
+
parts.push(`↑${formatTokens(m.outputTokens)}`);
|
|
292
|
+
return parts.join(" ");
|
|
293
|
+
};
|
|
294
|
+
const visible = () => expandedActive() && (currentAggregate()?.modelBreakdown.length ?? 0) > i;
|
|
295
|
+
// The wrapper box reserves marginTop even when its
|
|
296
|
+
// children collapse to height 0, leaving blank rows
|
|
297
|
+
// while collapsed. Collapse height AND margins here.
|
|
298
|
+
let wrapperNode;
|
|
299
|
+
const syncWrapper = () => {
|
|
300
|
+
if (disposed)
|
|
301
|
+
return;
|
|
302
|
+
if (!wrapperNode || wrapperNode.isDestroyed)
|
|
303
|
+
return;
|
|
304
|
+
const v = visible();
|
|
305
|
+
try {
|
|
306
|
+
wrapperNode.height = v ? "auto" : 0;
|
|
307
|
+
wrapperNode.marginTop = v ? 1 : 0;
|
|
308
|
+
wrapperNode.marginLeft = v ? 1 : 0;
|
|
309
|
+
}
|
|
310
|
+
catch { /* ignore */ }
|
|
311
|
+
};
|
|
312
|
+
const attachWrapper = (node) => {
|
|
313
|
+
wrapperNode = node;
|
|
314
|
+
syncWrapper();
|
|
315
|
+
};
|
|
316
|
+
// Wrapper syncs are unregistered wholesale by dispose() via
|
|
317
|
+
// rowSyncs.clear() (the old per-row onCleanup unregister went
|
|
318
|
+
// with the JSX scope).
|
|
319
|
+
registerRowSync(syncWrapper);
|
|
320
|
+
rows.push(element("box", {
|
|
321
|
+
ref: attachWrapper,
|
|
322
|
+
width: "100%",
|
|
323
|
+
flexDirection: "column",
|
|
324
|
+
marginLeft: 1,
|
|
325
|
+
marginTop: 1,
|
|
326
|
+
}, [
|
|
327
|
+
StatRow({
|
|
328
|
+
theme: props.theme,
|
|
329
|
+
label: modelLabel,
|
|
330
|
+
value: () => "",
|
|
331
|
+
dim: true,
|
|
332
|
+
registerSync: registerRowSync,
|
|
333
|
+
visible,
|
|
334
|
+
}),
|
|
335
|
+
StatRow({
|
|
336
|
+
theme: props.theme,
|
|
337
|
+
label: () => "",
|
|
338
|
+
value: modelValue,
|
|
339
|
+
dim: true,
|
|
340
|
+
registerSync: registerRowSync,
|
|
341
|
+
visible,
|
|
342
|
+
}),
|
|
343
|
+
]));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (rowVisible("speed")) {
|
|
347
|
+
rows.push(StatRow({
|
|
348
|
+
theme: props.theme,
|
|
349
|
+
label: "TPS",
|
|
350
|
+
value: speedValue,
|
|
351
|
+
accent: true,
|
|
352
|
+
icon: "⚡",
|
|
353
|
+
registerSync: registerRowSync,
|
|
354
|
+
visible: collapsedActive,
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
if (rowVisible("session")) {
|
|
358
|
+
rows.push(StatRow({
|
|
359
|
+
theme: props.theme,
|
|
360
|
+
label: "Session",
|
|
361
|
+
value: sessionValue,
|
|
362
|
+
icon: "◷",
|
|
363
|
+
registerSync: registerRowSync,
|
|
364
|
+
visible: collapsedActive,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
const contentBox = element("box", {
|
|
368
|
+
width: "100%",
|
|
369
|
+
flexDirection: "column",
|
|
370
|
+
}, rows);
|
|
371
|
+
const root = element("box", {
|
|
372
|
+
width: "100%",
|
|
373
|
+
flexDirection: "column",
|
|
374
|
+
height: sectionEnabled() ? "auto" : 0,
|
|
375
|
+
}, [headerBox, contentBox]);
|
|
376
|
+
return { node: root, dispose };
|
|
219
377
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/** @jsxImportSource @opentui/solid */
|
|
3
|
-
/** @jsxRuntime automatic */
|
|
4
|
-
import { onCleanup } from "solid-js";
|
|
1
|
+
import { element } from "./ui-element";
|
|
5
2
|
export function StatRow(props) {
|
|
6
|
-
let disposed = false;
|
|
7
3
|
let rowNode;
|
|
8
4
|
let labelNode;
|
|
9
5
|
let valueNode;
|
|
10
|
-
let unregisterSync;
|
|
11
6
|
const fg = () => {
|
|
12
7
|
if (props.warning)
|
|
13
8
|
return props.theme.warning;
|
|
@@ -24,8 +19,6 @@ export function StatRow(props) {
|
|
|
24
19
|
const visible = () => typeof props.visible === "function" ? props.visible() : props.visible !== false;
|
|
25
20
|
const content = () => `${props.icon ? `${props.icon} ` : ""}${value()}`;
|
|
26
21
|
const syncContent = () => {
|
|
27
|
-
if (disposed)
|
|
28
|
-
return;
|
|
29
22
|
const isVisible = visible();
|
|
30
23
|
if (rowNode && !rowNode.isDestroyed) {
|
|
31
24
|
rowNode.visible = isVisible;
|
|
@@ -45,7 +38,12 @@ export function StatRow(props) {
|
|
|
45
38
|
valueNode.content = isVisible ? content() : "";
|
|
46
39
|
valueNode.requestRender();
|
|
47
40
|
};
|
|
48
|
-
|
|
41
|
+
// No onCleanup here: the tree is built outside a dispose-owned JSX scope
|
|
42
|
+
// on the Node loader path. Row syncs stop when the parent
|
|
43
|
+
// SidebarMetrics.dispose() clears its rowSyncs set, and the renderables
|
|
44
|
+
// themselves are destroyed with the host slot tree (isDestroyed guards
|
|
45
|
+
// above cover the not-yet-disposed window).
|
|
46
|
+
props.registerSync?.(syncContent);
|
|
49
47
|
const attachRowNode = (node) => {
|
|
50
48
|
rowNode = node;
|
|
51
49
|
syncContent();
|
|
@@ -58,13 +56,22 @@ export function StatRow(props) {
|
|
|
58
56
|
valueNode = node;
|
|
59
57
|
syncContent();
|
|
60
58
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
return element("box", {
|
|
60
|
+
ref: attachRowNode,
|
|
61
|
+
width: "100%",
|
|
62
|
+
flexDirection: "row",
|
|
63
|
+
justifyContent: "space-between",
|
|
64
|
+
visible: visible(),
|
|
65
|
+
}, [
|
|
66
|
+
element("text", {
|
|
67
|
+
ref: attachLabelNode,
|
|
68
|
+
fg: props.theme.textMuted,
|
|
69
|
+
content: visible() ? label() : "",
|
|
70
|
+
}),
|
|
71
|
+
element("text", {
|
|
72
|
+
ref: attachValueNode,
|
|
73
|
+
fg: fg(),
|
|
74
|
+
content: content(),
|
|
75
|
+
}),
|
|
76
|
+
]);
|
|
70
77
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct element factory (mirrors oh-my-opencode-slim's `element()` helper).
|
|
3
|
+
*
|
|
4
|
+
* The opencode v2.0.26 Node loader cannot resolve the automatic-JSX entry of
|
|
5
|
+
* `@opentui/solid` from plugin dist, so no JSX may remain in the sources:
|
|
6
|
+
* trees are built by hand with the same call shapes OMO uses —
|
|
7
|
+
* `createElement` + `setProp` + `insert` from the `@opentui/solid` package
|
|
8
|
+
* root.
|
|
9
|
+
*
|
|
10
|
+
* Semantics that MUST stay identical to the old JSX emit:
|
|
11
|
+
* - props are applied, children are inserted, then an optional `ref` callback
|
|
12
|
+
* runs (JSX `spread` ran children -> ref -> props; running the ref AFTER the
|
|
13
|
+
* props lets the imperative registerSync pass win at mount — on every mount
|
|
14
|
+
* state the prop and the sync compute the same values, except where the sync
|
|
15
|
+
* is the one that knows the truth, e.g. collapse state / hidden model rows).
|
|
16
|
+
* - children are handed to `insert` with ONE array call, matching the JSX
|
|
17
|
+
* runtime's single-array `insertExpression`. Inserting string siblings
|
|
18
|
+
* one-by-one would wipe the previously inserted text node.
|
|
19
|
+
* - children must be pre-evaluated values (no accessor functions): this
|
|
20
|
+
* runtime's tree is non-reactive by design; dynamic values flow through the
|
|
21
|
+
* components' registerSync -> requestRender path instead.
|
|
22
|
+
*/
|
|
23
|
+
import { createElement, insert, setProp } from "@opentui/solid";
|
|
24
|
+
export function element(tag, props = {}, children = []) {
|
|
25
|
+
const node = createElement(tag);
|
|
26
|
+
let ref;
|
|
27
|
+
for (const [key, value] of Object.entries(props)) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
if (key === "ref") {
|
|
31
|
+
ref = value;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
setProp(node, key, value);
|
|
35
|
+
}
|
|
36
|
+
if (children.length > 0)
|
|
37
|
+
insert(node, children);
|
|
38
|
+
ref?.(node);
|
|
39
|
+
return node;
|
|
40
|
+
}
|
package/dist/tui-v2.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { jsx as _jsx } from "@opentui/solid/jsx-runtime";
|
|
2
1
|
/**
|
|
3
2
|
* OpenCode V2 TUI entry (dual-host compatibility).
|
|
4
3
|
*
|
|
@@ -39,8 +38,7 @@ import { jsx as _jsx } from "@opentui/solid/jsx-runtime";
|
|
|
39
38
|
* - message.updated passthrough (user-message turn anchor) — turn start is
|
|
40
39
|
* covered by session.execution.started / step.started
|
|
41
40
|
*/
|
|
42
|
-
|
|
43
|
-
/** @jsxRuntime automatic */
|
|
41
|
+
import { createElement } from "@opentui/solid";
|
|
44
42
|
import { createCollector } from "./collector";
|
|
45
43
|
import { getConfig } from "./config";
|
|
46
44
|
import { log } from "./logger";
|
|
@@ -271,8 +269,20 @@ export function setupTuiV2(ctx) {
|
|
|
271
269
|
render: (input) => {
|
|
272
270
|
const sessionID = input?.sessionID ?? "";
|
|
273
271
|
if (!sessionID)
|
|
274
|
-
return
|
|
275
|
-
|
|
272
|
+
return createElement("box");
|
|
273
|
+
const sidebar = SidebarMetrics({
|
|
274
|
+
sessionID,
|
|
275
|
+
collector,
|
|
276
|
+
refreshIntervalMs: config.refreshIntervalMs,
|
|
277
|
+
barConfig: config,
|
|
278
|
+
theme: theme,
|
|
279
|
+
controller,
|
|
280
|
+
requestRender,
|
|
281
|
+
});
|
|
282
|
+
// Component-owned timers/subscriptions are torn down from the
|
|
283
|
+
// setup cleanup path (the disposers array returned below).
|
|
284
|
+
disposers.push(sidebar.dispose);
|
|
285
|
+
return sidebar.node;
|
|
276
286
|
},
|
|
277
287
|
}));
|
|
278
288
|
return () => {
|
package/dist/tui.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { jsx as _jsx } from "@opentui/solid/jsx-runtime";
|
|
2
1
|
import { createCollector } from "./collector";
|
|
3
2
|
import { getConfig } from "./config";
|
|
4
3
|
import { log } from "./logger";
|
|
@@ -15,15 +14,35 @@ const plugin = async (api, _options, _meta) => {
|
|
|
15
14
|
const effectiveOrder = computeEffectiveOrder(seedRoot, PLUGIN_KEY, DEFAULT_SLOT_ORDER);
|
|
16
15
|
const prefs = resolveMetricsPrefs(seedRoot);
|
|
17
16
|
const controller = createMetricsSidebarController(prefs, () => api.renderer.requestRender());
|
|
17
|
+
// Explicit teardown for component-owned timers/subscriptions (replaces
|
|
18
|
+
// the components' old onCleanup, which is not reliable on the V2 Node
|
|
19
|
+
// loader path). Drained from the plugin dispose hook below.
|
|
20
|
+
const sidebarDisposers = [];
|
|
18
21
|
api.slots.register({
|
|
19
22
|
order: effectiveOrder,
|
|
20
23
|
slots: {
|
|
21
24
|
sidebar_content(ctx, props) {
|
|
22
|
-
|
|
25
|
+
const sidebar = SidebarMetrics({
|
|
26
|
+
sessionID: props.session_id,
|
|
27
|
+
collector,
|
|
28
|
+
refreshIntervalMs: config.refreshIntervalMs,
|
|
29
|
+
barConfig: config,
|
|
30
|
+
theme: ctx.theme.current,
|
|
31
|
+
controller: controller,
|
|
32
|
+
requestRender: () => api.renderer.requestRender(),
|
|
33
|
+
});
|
|
34
|
+
sidebarDisposers.push(sidebar.dispose);
|
|
35
|
+
return sidebar.node;
|
|
23
36
|
},
|
|
24
37
|
},
|
|
25
38
|
});
|
|
26
39
|
api.lifecycle.onDispose(() => {
|
|
40
|
+
for (const dispose of sidebarDisposers.splice(0)) {
|
|
41
|
+
try {
|
|
42
|
+
dispose();
|
|
43
|
+
}
|
|
44
|
+
catch { /* ignore */ }
|
|
45
|
+
}
|
|
27
46
|
collector.dispose();
|
|
28
47
|
});
|
|
29
48
|
log("opencode-metrics sidebar initialized");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-metricboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "OpenCode TUI sidebar plugin showing real-time LLM metrics (TPS, TTFT, tokens) with sub-agent × model breakdown",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"src/event-shapes.ts",
|
|
67
67
|
"src/components/SidebarMetrics.tsx",
|
|
68
68
|
"src/components/StatRow.tsx",
|
|
69
|
+
"src/components/ui-element.ts",
|
|
69
70
|
"src/scripts/init-tui-preferences.ts",
|
|
70
71
|
"README.md"
|
|
71
72
|
],
|
|
@@ -88,6 +89,10 @@
|
|
|
88
89
|
"dependencies": {
|
|
89
90
|
"comment-json": "^5.0.0"
|
|
90
91
|
},
|
|
92
|
+
"optionalDependencies": {
|
|
93
|
+
"@opentui/solid": "0.5.17",
|
|
94
|
+
"solid-js": "1.9.12"
|
|
95
|
+
},
|
|
91
96
|
"peerDependencies": {
|
|
92
97
|
"@opencode-ai/plugin": "*",
|
|
93
98
|
"@opencode-ai/sdk": "*",
|