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,379 +1,396 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
const
|
|
164
|
-
const m = currentAggregate()
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1
|
+
import { createMemo, createSignal, type JSX } from "solid-js"
|
|
2
|
+
import type { BoxRenderable, TextRenderable } from "@opentui/core"
|
|
3
|
+
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
|
+
import type { BarConfig, MetricsAggregate, ModelMetrics } from "../types"
|
|
5
|
+
import type { MetricsCollector } from "../collector"
|
|
6
|
+
import {
|
|
7
|
+
formatTokens,
|
|
8
|
+
formatDuration,
|
|
9
|
+
formatElapsed,
|
|
10
|
+
formatCacheRead,
|
|
11
|
+
} from "../metrics"
|
|
12
|
+
import { StatRow } from "./StatRow"
|
|
13
|
+
import { element } from "./ui-element"
|
|
14
|
+
import type { MetricsSidebarController } from "../tui-preferences"
|
|
15
|
+
|
|
16
|
+
// Upper bound on model-breakdown rows rendered in the sidebar. This runtime's
|
|
17
|
+
// tree is not reactive, so rows are pre-rendered once and toggled via
|
|
18
|
+
// registerSync; more sub-agents than this simply scroll off.
|
|
19
|
+
const MAX_MODEL_ROWS = 8
|
|
20
|
+
|
|
21
|
+
interface SidebarMetricsProps {
|
|
22
|
+
sessionID: string
|
|
23
|
+
collector: MetricsCollector
|
|
24
|
+
refreshIntervalMs: number
|
|
25
|
+
barConfig: BarConfig
|
|
26
|
+
theme: TuiThemeCurrent
|
|
27
|
+
controller: MetricsSidebarController
|
|
28
|
+
requestRender?: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SidebarMetricsInstance {
|
|
32
|
+
/** Root renderable for the host slot. Treated as the host's `JSX.Element` (same lie the old jsx-runtime types told). */
|
|
33
|
+
node: JSX.Element
|
|
34
|
+
/** Clears timers/subscriptions/syncs. Call from the plugin setup cleanup path. */
|
|
35
|
+
dispose: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SidebarMetrics(props: SidebarMetricsProps): SidebarMetricsInstance {
|
|
39
|
+
let disposed = false
|
|
40
|
+
let refreshQueued = false
|
|
41
|
+
let interval: ReturnType<typeof setInterval> | undefined
|
|
42
|
+
let unsub = () => {}
|
|
43
|
+
let unsubController = () => {}
|
|
44
|
+
const rowSyncs = new Set<() => void>()
|
|
45
|
+
const registerRowSync = (sync: () => void) => {
|
|
46
|
+
if (disposed) return () => {}
|
|
47
|
+
rowSyncs.add(sync)
|
|
48
|
+
return () => rowSyncs.delete(sync)
|
|
49
|
+
}
|
|
50
|
+
const syncRows = () => {
|
|
51
|
+
if (disposed) return
|
|
52
|
+
for (const sync of rowSyncs) sync()
|
|
53
|
+
}
|
|
54
|
+
const [tick, setTick] = createSignal(0)
|
|
55
|
+
const bump = () => {
|
|
56
|
+
if (disposed) return
|
|
57
|
+
setTick((t) => t + 1)
|
|
58
|
+
syncRows()
|
|
59
|
+
if (refreshQueued) return
|
|
60
|
+
refreshQueued = true
|
|
61
|
+
queueMicrotask(() => {
|
|
62
|
+
refreshQueued = false
|
|
63
|
+
if (disposed) return
|
|
64
|
+
syncRows()
|
|
65
|
+
props.requestRender?.()
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Explicit disposal replacing the old onCleanup: the V2 Node loader builds
|
|
70
|
+
// this tree outside a dispose-owned JSX scope, so onCleanup was wired to a
|
|
71
|
+
// scope we do not control. The setup cleanup path (tui.tsx onDispose /
|
|
72
|
+
// tui-v2.tsx setup return) calls this instead.
|
|
73
|
+
const dispose = () => {
|
|
74
|
+
if (disposed) return
|
|
75
|
+
disposed = true
|
|
76
|
+
refreshQueued = false
|
|
77
|
+
rowSyncs.clear()
|
|
78
|
+
if (interval !== undefined) clearInterval(interval)
|
|
79
|
+
unsub()
|
|
80
|
+
unsubController()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interval = setInterval(bump, props.refreshIntervalMs)
|
|
84
|
+
unsub = props.collector.subscribe(bump)
|
|
85
|
+
unsubController = props.controller.subscribe(bump)
|
|
86
|
+
|
|
87
|
+
const sectionEnabled = createMemo(() => {
|
|
88
|
+
tick()
|
|
89
|
+
return props.controller.prefs().section.enabled
|
|
90
|
+
})
|
|
91
|
+
const requestNow = (m: MetricsAggregate): number => {
|
|
92
|
+
const live = performance.now()
|
|
93
|
+
return m.isComplete && m.completeTime !== null ? m.completeTime : live
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rowVisible = (key: keyof BarConfig["visible"]): boolean => {
|
|
97
|
+
const barVis = props.barConfig.visible
|
|
98
|
+
const rowPrefs = props.controller.prefs().rows
|
|
99
|
+
return barVis[key] !== false && rowPrefs[key as keyof typeof rowPrefs] !== false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const collapsed = createMemo(() => {
|
|
103
|
+
tick()
|
|
104
|
+
return props.controller.collapsed()
|
|
105
|
+
})
|
|
106
|
+
const headerLabel = () => props.controller.prefs().section.label
|
|
107
|
+
// This runtime's tree is non-reactive: the header <text> nodes are built
|
|
108
|
+
// once at mount, so the ▶/▼ arrow would freeze on its initial value. Two
|
|
109
|
+
// static bold nodes (collapsed/expanded) are pre-rendered and toggled
|
|
110
|
+
// imperatively through registerSync — the same mechanism as the rows.
|
|
111
|
+
// Hidden node must be zero-sized in BOTH axes: visible=false only
|
|
112
|
+
// suppresses painting, while yoga still reserves its box, which would
|
|
113
|
+
// offset the visible label. No value imports from @opentui/core beyond
|
|
114
|
+
// what mount already exercised, so version drift can't break the plugin.
|
|
115
|
+
let collapsedHeaderNode: TextRenderable | undefined
|
|
116
|
+
let expandedHeaderNode: TextRenderable | undefined
|
|
117
|
+
const syncHeader = () => {
|
|
118
|
+
if (disposed) return
|
|
119
|
+
const isCollapsed = collapsed()
|
|
120
|
+
for (const [node, visible] of [
|
|
121
|
+
[collapsedHeaderNode, isCollapsed],
|
|
122
|
+
[expandedHeaderNode, !isCollapsed],
|
|
123
|
+
] as const) {
|
|
124
|
+
if (!node || node.isDestroyed) continue
|
|
125
|
+
try {
|
|
126
|
+
node.visible = visible
|
|
127
|
+
if (visible) {
|
|
128
|
+
node.width = "auto"
|
|
129
|
+
node.height = "auto"
|
|
130
|
+
} else {
|
|
131
|
+
node.width = 0
|
|
132
|
+
node.height = 0
|
|
133
|
+
}
|
|
134
|
+
} catch { /* cosmetic — ignore layout failures */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Unregistered wholesale by dispose() via rowSyncs.clear() (the old
|
|
138
|
+
// per-sync onCleanup unregister went with the JSX scope).
|
|
139
|
+
registerRowSync(syncHeader)
|
|
140
|
+
const attachCollapsedHeaderNode = (node: TextRenderable) => {
|
|
141
|
+
collapsedHeaderNode = node
|
|
142
|
+
syncHeader()
|
|
143
|
+
}
|
|
144
|
+
const attachExpandedHeaderNode = (node: TextRenderable) => {
|
|
145
|
+
expandedHeaderNode = node
|
|
146
|
+
syncHeader()
|
|
147
|
+
}
|
|
148
|
+
const toggleCollapsed = () => props.controller.toggleCollapsed()
|
|
149
|
+
const attachBoxToggle = (node: BoxRenderable) => {
|
|
150
|
+
node.onMouseDown = toggleCollapsed
|
|
151
|
+
}
|
|
152
|
+
const currentScope = () => props.controller.prefs().scope
|
|
153
|
+
const currentAggregate = () => props.collector.getAggregate(props.sessionID, currentScope())
|
|
154
|
+
const hasAggregate = () => currentAggregate() !== null
|
|
155
|
+
const expandedActive = () => !collapsed() && hasAggregate()
|
|
156
|
+
const expandedIdle = () => !collapsed() && !hasAggregate()
|
|
157
|
+
const collapsedActive = () => collapsed() && hasAggregate()
|
|
158
|
+
const frozenNow = (): number => {
|
|
159
|
+
const m = currentAggregate()
|
|
160
|
+
if (!m) return performance.now()
|
|
161
|
+
return requestNow(m)
|
|
162
|
+
}
|
|
163
|
+
const speedValue = () => {
|
|
164
|
+
const m = currentAggregate()
|
|
165
|
+
return m?.liveTps === null || m?.liveTps === undefined ? "—" : `${m.liveTps.toFixed(1)} t/s`
|
|
166
|
+
}
|
|
167
|
+
const elapsedValue = () => {
|
|
168
|
+
const m = currentAggregate()
|
|
169
|
+
return formatElapsed(m ? frozenNow() - m.requestStartTime : 0)
|
|
170
|
+
}
|
|
171
|
+
const ttftValue = () => {
|
|
172
|
+
const ttft = currentAggregate()?.ttft ?? null
|
|
173
|
+
return ttft !== null ? formatDuration(ttft) : "--"
|
|
174
|
+
}
|
|
175
|
+
const tokenValue = () => {
|
|
176
|
+
const m = currentAggregate()
|
|
177
|
+
const inputTokens = m?.inputTokens ?? 0
|
|
178
|
+
const outputTokens = m?.outputTokens ?? 0
|
|
179
|
+
return `${rowVisible("input") ? `↓ ${formatTokens(inputTokens)} in` : ""}${rowVisible("input") && rowVisible("output") ? " " : ""}${rowVisible("output") ? `↑ ${formatTokens(outputTokens)} out` : ""}`
|
|
180
|
+
}
|
|
181
|
+
const cacheValue = () => {
|
|
182
|
+
const m = currentAggregate()
|
|
183
|
+
return formatCacheRead(m?.cacheReadTokens ?? 0, m?.cacheReadCompleteness ?? "unknown")
|
|
184
|
+
}
|
|
185
|
+
const sessionValue = () => {
|
|
186
|
+
return formatElapsed(props.collector.getSessionElapsedMs(props.sessionID, currentScope(), performance.now()))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- tree (same hierarchy/spacing/order as the old JSX) ------------------
|
|
190
|
+
const headerBox = element("box", {
|
|
191
|
+
width: "100%",
|
|
192
|
+
flexDirection: "row",
|
|
193
|
+
alignItems: "center",
|
|
194
|
+
ref: attachBoxToggle,
|
|
195
|
+
}, [
|
|
196
|
+
element("text", {
|
|
197
|
+
ref: attachCollapsedHeaderNode,
|
|
198
|
+
fg: props.theme.text,
|
|
199
|
+
visible: false,
|
|
200
|
+
}, [
|
|
201
|
+
element("b", {}, ["▶ ", headerLabel()]),
|
|
202
|
+
]),
|
|
203
|
+
element("text", {
|
|
204
|
+
ref: attachExpandedHeaderNode,
|
|
205
|
+
fg: props.theme.text,
|
|
206
|
+
}, [
|
|
207
|
+
element("b", {}, ["▼ ", headerLabel()]),
|
|
208
|
+
]),
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
const rows: unknown[] = []
|
|
212
|
+
rows.push(StatRow({
|
|
213
|
+
theme: props.theme,
|
|
214
|
+
label: "Status",
|
|
215
|
+
value: "No active request",
|
|
216
|
+
dim: true,
|
|
217
|
+
icon: "○",
|
|
218
|
+
visible: expandedIdle,
|
|
219
|
+
registerSync: registerRowSync,
|
|
220
|
+
}))
|
|
221
|
+
if (rowVisible("speed")) {
|
|
222
|
+
rows.push(StatRow({
|
|
223
|
+
theme: props.theme,
|
|
224
|
+
label: "TPS",
|
|
225
|
+
value: speedValue,
|
|
226
|
+
accent: true,
|
|
227
|
+
icon: "⚡",
|
|
228
|
+
registerSync: registerRowSync,
|
|
229
|
+
visible: expandedActive,
|
|
230
|
+
}))
|
|
231
|
+
}
|
|
232
|
+
if (rowVisible("elapsed")) {
|
|
233
|
+
rows.push(StatRow({
|
|
234
|
+
theme: props.theme,
|
|
235
|
+
label: "Elapsed",
|
|
236
|
+
value: elapsedValue,
|
|
237
|
+
icon: "▹",
|
|
238
|
+
registerSync: registerRowSync,
|
|
239
|
+
visible: expandedActive,
|
|
240
|
+
}))
|
|
241
|
+
}
|
|
242
|
+
if (rowVisible("ttft")) {
|
|
243
|
+
rows.push(StatRow({
|
|
244
|
+
theme: props.theme,
|
|
245
|
+
label: "TTFT",
|
|
246
|
+
value: ttftValue,
|
|
247
|
+
icon: "⏱",
|
|
248
|
+
registerSync: registerRowSync,
|
|
249
|
+
visible: expandedActive,
|
|
250
|
+
}))
|
|
251
|
+
}
|
|
252
|
+
if (rowVisible("input") || rowVisible("output")) {
|
|
253
|
+
rows.push(StatRow({
|
|
254
|
+
theme: props.theme,
|
|
255
|
+
label: "Tokens",
|
|
256
|
+
value: tokenValue,
|
|
257
|
+
registerSync: registerRowSync,
|
|
258
|
+
visible: expandedActive,
|
|
259
|
+
}))
|
|
260
|
+
}
|
|
261
|
+
if (rowVisible("cache")) {
|
|
262
|
+
rows.push(StatRow({
|
|
263
|
+
theme: props.theme,
|
|
264
|
+
label: "Cache",
|
|
265
|
+
value: cacheValue,
|
|
266
|
+
dim: true,
|
|
267
|
+
icon: "○",
|
|
268
|
+
registerSync: registerRowSync,
|
|
269
|
+
visible: expandedActive,
|
|
270
|
+
}))
|
|
271
|
+
}
|
|
272
|
+
if (rowVisible("session")) {
|
|
273
|
+
rows.push(StatRow({
|
|
274
|
+
theme: props.theme,
|
|
275
|
+
label: "Session",
|
|
276
|
+
value: sessionValue,
|
|
277
|
+
icon: "◷",
|
|
278
|
+
registerSync: registerRowSync,
|
|
279
|
+
visible: expandedActive,
|
|
280
|
+
}))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Model breakdown rows for tree scope. This runtime's tree is NOT
|
|
284
|
+
// reactive — it is built once at mount, so inline .map() evaluates once:
|
|
285
|
+
// rows for sub-agents spawned later would never appear. Fix: pre-render a
|
|
286
|
+
// fixed number of rows that pull their data live from currentAggregate()
|
|
287
|
+
// on every registerSync tick, exactly like the static Speed/TTFT rows.
|
|
288
|
+
if (rowVisible("modelBreakdown")) {
|
|
289
|
+
for (let i = 0; i < MAX_MODEL_ROWS; i++) {
|
|
290
|
+
// Line 1: short model name + ×N session count.
|
|
291
|
+
const modelLabel = () => {
|
|
292
|
+
const m = currentAggregate()?.modelBreakdown[i]
|
|
293
|
+
if (!m) return ""
|
|
294
|
+
const count = m.sessionCount > 1 ? ` ×${m.sessionCount}` : ""
|
|
295
|
+
return `${m.modelID}${count}`
|
|
296
|
+
}
|
|
297
|
+
// Line 2: metrics for that model group. Compact
|
|
298
|
+
// format (no spaces after icons, no unit suffixes,
|
|
299
|
+
// single-space separators) so the data line fits the
|
|
300
|
+
// narrow sidebar on ONE line without right-edge
|
|
301
|
+
// truncation.
|
|
302
|
+
const modelValue = () => {
|
|
303
|
+
const m = currentAggregate()?.modelBreakdown[i]
|
|
304
|
+
if (!m) return ""
|
|
305
|
+
const parts: string[] = []
|
|
306
|
+
if (rowVisible("speed")) parts.push(`⚡${m.liveTps !== null ? m.liveTps.toFixed(1) : "—"}`)
|
|
307
|
+
if (rowVisible("ttft")) parts.push(`⏱${m.ttft !== null ? " " + formatDuration(m.ttft) : " --"}`)
|
|
308
|
+
if (rowVisible("input")) parts.push(`↓${formatTokens(m.inputTokens)}`)
|
|
309
|
+
if (rowVisible("output")) parts.push(`↑${formatTokens(m.outputTokens)}`)
|
|
310
|
+
return parts.join(" ")
|
|
311
|
+
}
|
|
312
|
+
const visible = () => expandedActive() && (currentAggregate()?.modelBreakdown.length ?? 0) > i
|
|
313
|
+
// The wrapper box reserves marginTop even when its
|
|
314
|
+
// children collapse to height 0, leaving blank rows
|
|
315
|
+
// while collapsed. Collapse height AND margins here.
|
|
316
|
+
let wrapperNode: BoxRenderable | undefined
|
|
317
|
+
const syncWrapper = () => {
|
|
318
|
+
if (disposed) return
|
|
319
|
+
if (!wrapperNode || wrapperNode.isDestroyed) return
|
|
320
|
+
const v = visible()
|
|
321
|
+
try {
|
|
322
|
+
wrapperNode.height = v ? "auto" : 0
|
|
323
|
+
wrapperNode.marginTop = v ? 1 : 0
|
|
324
|
+
wrapperNode.marginLeft = v ? 1 : 0
|
|
325
|
+
} catch { /* ignore */ }
|
|
326
|
+
}
|
|
327
|
+
const attachWrapper = (node: BoxRenderable) => {
|
|
328
|
+
wrapperNode = node
|
|
329
|
+
syncWrapper()
|
|
330
|
+
}
|
|
331
|
+
// Wrapper syncs are unregistered wholesale by dispose() via
|
|
332
|
+
// rowSyncs.clear() (the old per-row onCleanup unregister went
|
|
333
|
+
// with the JSX scope).
|
|
334
|
+
registerRowSync(syncWrapper)
|
|
335
|
+
rows.push(element<BoxRenderable>("box", {
|
|
336
|
+
ref: attachWrapper,
|
|
337
|
+
width: "100%",
|
|
338
|
+
flexDirection: "column",
|
|
339
|
+
marginLeft: 1,
|
|
340
|
+
marginTop: 1,
|
|
341
|
+
}, [
|
|
342
|
+
StatRow({
|
|
343
|
+
theme: props.theme,
|
|
344
|
+
label: modelLabel,
|
|
345
|
+
value: () => "",
|
|
346
|
+
dim: true,
|
|
347
|
+
registerSync: registerRowSync,
|
|
348
|
+
visible,
|
|
349
|
+
}),
|
|
350
|
+
StatRow({
|
|
351
|
+
theme: props.theme,
|
|
352
|
+
label: () => "",
|
|
353
|
+
value: modelValue,
|
|
354
|
+
dim: true,
|
|
355
|
+
registerSync: registerRowSync,
|
|
356
|
+
visible,
|
|
357
|
+
}),
|
|
358
|
+
]))
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (rowVisible("speed")) {
|
|
363
|
+
rows.push(StatRow({
|
|
364
|
+
theme: props.theme,
|
|
365
|
+
label: "TPS",
|
|
366
|
+
value: speedValue,
|
|
367
|
+
accent: true,
|
|
368
|
+
icon: "⚡",
|
|
369
|
+
registerSync: registerRowSync,
|
|
370
|
+
visible: collapsedActive,
|
|
371
|
+
}))
|
|
372
|
+
}
|
|
373
|
+
if (rowVisible("session")) {
|
|
374
|
+
rows.push(StatRow({
|
|
375
|
+
theme: props.theme,
|
|
376
|
+
label: "Session",
|
|
377
|
+
value: sessionValue,
|
|
378
|
+
icon: "◷",
|
|
379
|
+
registerSync: registerRowSync,
|
|
380
|
+
visible: collapsedActive,
|
|
381
|
+
}))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const contentBox = element("box", {
|
|
385
|
+
width: "100%",
|
|
386
|
+
flexDirection: "column",
|
|
387
|
+
}, rows)
|
|
388
|
+
|
|
389
|
+
const root = element("box", {
|
|
390
|
+
width: "100%",
|
|
391
|
+
flexDirection: "column",
|
|
392
|
+
height: sectionEnabled() ? "auto" : 0,
|
|
393
|
+
}, [headerBox, contentBox])
|
|
394
|
+
|
|
395
|
+
return { node: root as unknown as JSX.Element, dispose }
|
|
396
|
+
}
|