oc-metricboard 0.1.21 → 0.1.23
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 +5 -2
- 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,83 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const attachRowNode = (node: BoxRenderable) => {
|
|
57
|
-
rowNode = node
|
|
58
|
-
syncContent()
|
|
59
|
-
}
|
|
60
|
-
const attachLabelNode = (node: TextRenderable) => {
|
|
61
|
-
labelNode = node
|
|
62
|
-
syncContent()
|
|
63
|
-
}
|
|
64
|
-
const attachValueNode = (node: TextRenderable) => {
|
|
65
|
-
valueNode = node
|
|
66
|
-
syncContent()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1
|
+
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import type { BoxRenderable, TextRenderable } from "@opentui/core"
|
|
3
|
+
import { element } from "./ui-element"
|
|
4
|
+
|
|
5
|
+
interface StatRowProps {
|
|
6
|
+
theme: TuiThemeCurrent
|
|
7
|
+
label: string | (() => string)
|
|
8
|
+
value: string | (() => string)
|
|
9
|
+
accent?: boolean
|
|
10
|
+
warning?: boolean
|
|
11
|
+
success?: boolean
|
|
12
|
+
dim?: boolean
|
|
13
|
+
icon?: string
|
|
14
|
+
visible?: boolean | (() => boolean)
|
|
15
|
+
registerSync?: (sync: () => void) => () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function StatRow(props: StatRowProps) {
|
|
19
|
+
let rowNode: BoxRenderable | undefined
|
|
20
|
+
let labelNode: TextRenderable | undefined
|
|
21
|
+
let valueNode: TextRenderable | undefined
|
|
22
|
+
const fg = () => {
|
|
23
|
+
if (props.warning) return props.theme.warning
|
|
24
|
+
if (props.success) return props.theme.success ?? props.theme.accent
|
|
25
|
+
if (props.accent) return props.theme.accent
|
|
26
|
+
if (props.dim) return props.theme.textMuted
|
|
27
|
+
return props.theme.text
|
|
28
|
+
}
|
|
29
|
+
const label = () => typeof props.label === "function" ? props.label() : props.label
|
|
30
|
+
const value = () => typeof props.value === "function" ? props.value() : props.value
|
|
31
|
+
const visible = () => typeof props.visible === "function" ? props.visible() : props.visible !== false
|
|
32
|
+
const content = () => `${props.icon ? `${props.icon} ` : ""}${value()}`
|
|
33
|
+
const syncContent = () => {
|
|
34
|
+
const isVisible = visible()
|
|
35
|
+
if (rowNode && !rowNode.isDestroyed) {
|
|
36
|
+
rowNode.visible = isVisible
|
|
37
|
+
// visible=false only suppresses rendering — the yoga layout node
|
|
38
|
+
// still reserves height, leaving blank gaps when a row is hidden
|
|
39
|
+
// (e.g. expanded rows while the panel is collapsed). Collapse the
|
|
40
|
+
// height so hidden rows truly take no space.
|
|
41
|
+
try {
|
|
42
|
+
rowNode.height = isVisible ? "auto" : 0
|
|
43
|
+
} catch { /* not all renderables accept height */ }
|
|
44
|
+
}
|
|
45
|
+
if (labelNode && !labelNode.isDestroyed) labelNode.content = isVisible ? label() : ""
|
|
46
|
+
if (!valueNode || valueNode.isDestroyed) return
|
|
47
|
+
valueNode.content = isVisible ? content() : ""
|
|
48
|
+
valueNode.requestRender()
|
|
49
|
+
}
|
|
50
|
+
// No onCleanup here: the tree is built outside a dispose-owned JSX scope
|
|
51
|
+
// on the Node loader path. Row syncs stop when the parent
|
|
52
|
+
// SidebarMetrics.dispose() clears its rowSyncs set, and the renderables
|
|
53
|
+
// themselves are destroyed with the host slot tree (isDestroyed guards
|
|
54
|
+
// above cover the not-yet-disposed window).
|
|
55
|
+
props.registerSync?.(syncContent)
|
|
56
|
+
const attachRowNode = (node: BoxRenderable) => {
|
|
57
|
+
rowNode = node
|
|
58
|
+
syncContent()
|
|
59
|
+
}
|
|
60
|
+
const attachLabelNode = (node: TextRenderable) => {
|
|
61
|
+
labelNode = node
|
|
62
|
+
syncContent()
|
|
63
|
+
}
|
|
64
|
+
const attachValueNode = (node: TextRenderable) => {
|
|
65
|
+
valueNode = node
|
|
66
|
+
syncContent()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return element<BoxRenderable>("box", {
|
|
70
|
+
ref: attachRowNode,
|
|
71
|
+
width: "100%",
|
|
72
|
+
flexDirection: "row",
|
|
73
|
+
justifyContent: "space-between",
|
|
74
|
+
visible: visible(),
|
|
75
|
+
}, [
|
|
76
|
+
element<TextRenderable>("text", {
|
|
77
|
+
ref: attachLabelNode,
|
|
78
|
+
fg: props.theme.textMuted,
|
|
79
|
+
content: visible() ? label() : "",
|
|
80
|
+
}),
|
|
81
|
+
element<TextRenderable>("text", {
|
|
82
|
+
ref: attachValueNode,
|
|
83
|
+
fg: fg(),
|
|
84
|
+
content: content(),
|
|
85
|
+
}),
|
|
86
|
+
])
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
import type { BaseRenderable } from "@opentui/core"
|
|
25
|
+
|
|
26
|
+
export function element<T extends BaseRenderable = BaseRenderable>(
|
|
27
|
+
tag: string,
|
|
28
|
+
props: Record<string, unknown> = {},
|
|
29
|
+
children: unknown[] = [],
|
|
30
|
+
): T {
|
|
31
|
+
const node = createElement(tag)
|
|
32
|
+
let ref: ((node: BaseRenderable) => void) | undefined
|
|
33
|
+
for (const [key, value] of Object.entries(props)) {
|
|
34
|
+
if (value === undefined) continue
|
|
35
|
+
if (key === "ref") {
|
|
36
|
+
ref = value as typeof ref
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
setProp(node, key, value)
|
|
40
|
+
}
|
|
41
|
+
if (children.length > 0) insert(node, children)
|
|
42
|
+
ref?.(node)
|
|
43
|
+
return node as T
|
|
44
|
+
}
|
package/src/tui-v2.tsx
CHANGED
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
* - message.updated passthrough (user-message turn anchor) — turn start is
|
|
39
39
|
* covered by session.execution.started / step.started
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
/** @jsxRuntime automatic */
|
|
41
|
+
import { createElement } from "@opentui/solid"
|
|
43
42
|
import { createCollector, type MetricsCollector } from "./collector"
|
|
44
43
|
import type { MetricsEventApi } from "./event-bus"
|
|
45
44
|
import { getConfig } from "./config"
|
|
@@ -346,18 +345,20 @@ export function setupTuiV2(ctx: V2Context): V2Cleanup {
|
|
|
346
345
|
append: "sidebar.content",
|
|
347
346
|
render: (input) => {
|
|
348
347
|
const sessionID = input?.sessionID ?? ""
|
|
349
|
-
if (!sessionID) return
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)
|
|
348
|
+
if (!sessionID) return createElement("box")
|
|
349
|
+
const sidebar = SidebarMetrics({
|
|
350
|
+
sessionID,
|
|
351
|
+
collector,
|
|
352
|
+
refreshIntervalMs: config.refreshIntervalMs,
|
|
353
|
+
barConfig: config,
|
|
354
|
+
theme: theme as never,
|
|
355
|
+
controller,
|
|
356
|
+
requestRender,
|
|
357
|
+
})
|
|
358
|
+
// Component-owned timers/subscriptions are torn down from the
|
|
359
|
+
// setup cleanup path (the disposers array returned below).
|
|
360
|
+
disposers.push(sidebar.dispose)
|
|
361
|
+
return sidebar.node
|
|
361
362
|
},
|
|
362
363
|
})
|
|
363
364
|
)
|
package/src/tui.tsx
CHANGED
|
@@ -1,68 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "./tui-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginMeta } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import type { PluginOptions } from "@opencode-ai/plugin"
|
|
3
|
+
import { createCollector } from "./collector"
|
|
4
|
+
import { getConfig } from "./config"
|
|
5
|
+
import { log } from "./logger"
|
|
6
|
+
import { SidebarMetrics } from "./components/SidebarMetrics"
|
|
7
|
+
import {
|
|
8
|
+
computeEffectiveOrder,
|
|
9
|
+
createMetricsSidebarController,
|
|
10
|
+
DEFAULT_SLOT_ORDER,
|
|
11
|
+
PLUGIN_KEY,
|
|
12
|
+
resolveMetricsPrefs,
|
|
13
|
+
} from "./tui-preferences"
|
|
14
|
+
import { readTuiPreferencesFileSync } from "./tui-prefs-io"
|
|
15
|
+
import { setupTuiV2 } from "./tui-v2"
|
|
16
|
+
|
|
17
|
+
const plugin: TuiPlugin = async (api: TuiPluginApi, _options: PluginOptions | undefined, _meta: TuiPluginMeta) => {
|
|
18
|
+
const config = getConfig()
|
|
19
|
+
const collector = createCollector(api, config, log)
|
|
20
|
+
|
|
21
|
+
// Sync-read preferences at slot mount so the sidebar renders at its final
|
|
22
|
+
// order and collapse state on the first paint (no async flicker).
|
|
23
|
+
const seedRoot = readTuiPreferencesFileSync()
|
|
24
|
+
const effectiveOrder = computeEffectiveOrder(seedRoot, PLUGIN_KEY, DEFAULT_SLOT_ORDER)
|
|
25
|
+
const prefs = resolveMetricsPrefs(seedRoot)
|
|
26
|
+
const controller = createMetricsSidebarController(prefs, () => api.renderer.requestRender())
|
|
27
|
+
|
|
28
|
+
// Explicit teardown for component-owned timers/subscriptions (replaces
|
|
29
|
+
// the components' old onCleanup, which is not reliable on the V2 Node
|
|
30
|
+
// loader path). Drained from the plugin dispose hook below.
|
|
31
|
+
const sidebarDisposers: Array<() => void> = []
|
|
32
|
+
|
|
33
|
+
api.slots.register({
|
|
34
|
+
order: effectiveOrder,
|
|
35
|
+
slots: {
|
|
36
|
+
sidebar_content(ctx, props) {
|
|
37
|
+
const sidebar = SidebarMetrics({
|
|
38
|
+
sessionID: props.session_id,
|
|
39
|
+
collector,
|
|
40
|
+
refreshIntervalMs: config.refreshIntervalMs,
|
|
41
|
+
barConfig: config,
|
|
42
|
+
theme: ctx.theme.current,
|
|
43
|
+
controller: controller,
|
|
44
|
+
requestRender: () => api.renderer.requestRender(),
|
|
45
|
+
})
|
|
46
|
+
sidebarDisposers.push(sidebar.dispose)
|
|
47
|
+
return sidebar.node
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
api.lifecycle.onDispose(() => {
|
|
53
|
+
for (const dispose of sidebarDisposers.splice(0)) {
|
|
54
|
+
try {
|
|
55
|
+
dispose()
|
|
56
|
+
} catch { /* ignore */ }
|
|
57
|
+
}
|
|
58
|
+
collector.dispose()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
log("opencode-metrics sidebar initialized")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dual-host TUI module:
|
|
66
|
+
* - OpenCode V1 (1.18.x) validates `{ id, tui }` and ignores extra keys.
|
|
67
|
+
* - OpenCode V2 (2.0.x) validates `{ id, setup }` and rejects V1-only modules.
|
|
68
|
+
* Exporting both keys from one `./tui` module satisfies both hosts.
|
|
69
|
+
*/
|
|
70
|
+
const pluginModule: { id: string; tui: TuiPlugin; setup: typeof setupTuiV2 } = {
|
|
71
|
+
id: "oc-metricboard",
|
|
72
|
+
tui: plugin,
|
|
73
|
+
setup: setupTuiV2,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default pluginModule
|