opencode-tui-stats 0.4.2 → 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 +949 -0
- package/package.json +5 -4
- package/tracker.ts +0 -768
- package/tui.tsx +0 -215
package/tui.tsx
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
|
-
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Accessor } from "solid-js"
|
|
4
|
-
import {
|
|
5
|
-
clearLastPanelController,
|
|
6
|
-
createTracker,
|
|
7
|
-
setLastPanelController,
|
|
8
|
-
type ModelRow,
|
|
9
|
-
type TrackerHandle,
|
|
10
|
-
} from "./tracker"
|
|
11
|
-
|
|
12
|
-
/** Detail lines sit three cells in from the fold marker, visually owned by the row above. */
|
|
13
|
-
const DETAIL_INDENT = " "
|
|
14
|
-
/** Fixed cells keep every label on the same column across rows without measuring the panel. */
|
|
15
|
-
const RATE_CELL = 4
|
|
16
|
-
const TTFT_CELL = 6
|
|
17
|
-
const CACHE_CELL = 6
|
|
18
|
-
|
|
19
|
-
/** Split `provider/model` so the provider can be de-emphasised. Model ids may contain slashes, so
|
|
20
|
-
* only the first slash separates the provider. When the model already starts with the provider name
|
|
21
|
-
* (`deepseek/deepseek-flash`) the redundant prefix is dropped. */
|
|
22
|
-
function splitLabel(label: string) {
|
|
23
|
-
const slash = label.indexOf("/")
|
|
24
|
-
if (slash <= 0) return { provider: "", model: label }
|
|
25
|
-
const provider = label.slice(0, slash)
|
|
26
|
-
const model = label.slice(slash + 1)
|
|
27
|
-
if (model.toLowerCase().startsWith(provider.toLowerCase())) return { provider: "", model }
|
|
28
|
-
return { provider: `${provider}/`, model }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function rateCell(value: string | undefined) {
|
|
32
|
-
return value === undefined ? "—" : `${value.padStart(RATE_CELL)}t/s`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function ttftCell(value: string | undefined) {
|
|
36
|
-
return (value ?? "—").padEnd(TTFT_CELL)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function cacheCell(value: string | undefined) {
|
|
40
|
-
if (value === undefined) return "—".padStart(CACHE_CELL)
|
|
41
|
-
const percent = value.endsWith("%") ? value.slice(0, -1) : value
|
|
42
|
-
return `${percent.padStart(5)}%`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** A single model row: fold marker + summary line, plus its detail lines when expanded. */
|
|
46
|
-
function ModelRowView(props: { api: TuiPluginApi; row: ModelRow; expanded: boolean; onToggle: () => void }) {
|
|
47
|
-
// Guard against a drag-select being read as a click: only toggle when the press and the
|
|
48
|
-
// release land on the same cell.
|
|
49
|
-
let pressed: { x: number; y: number } | undefined
|
|
50
|
-
|
|
51
|
-
const handleMouseDown = (event: { x: number; y: number }) => {
|
|
52
|
-
pressed = { x: event.x, y: event.y }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const handleMouseUp = (event: { x: number; y: number }) => {
|
|
56
|
-
const start = pressed
|
|
57
|
-
pressed = undefined
|
|
58
|
-
if (start && (start.x !== event.x || start.y !== event.y)) return
|
|
59
|
-
props.onToggle()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const theme = () => props.api.theme.current
|
|
63
|
-
const parts = () => splitLabel(props.row.label)
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<box flexDirection="column" onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
|
|
67
|
-
<text wrapMode="none" truncate>
|
|
68
|
-
{/* The most recently used model gets a green dot; other rows keep a blank slot so
|
|
69
|
-
* names stay aligned without spending extra width on a second marker. */}
|
|
70
|
-
<span style={{ fg: theme().success } as Record<string, unknown>}>{props.row.active ? "● " : " "}</span>
|
|
71
|
-
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>{`${props.expanded ? "▼" : "▶"} `}</span>
|
|
72
|
-
<Show when={props.row.subagent}>
|
|
73
|
-
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>{"↳ "}</span>
|
|
74
|
-
</Show>
|
|
75
|
-
<Show when={parts().provider}>
|
|
76
|
-
{/* Dimmed foreground sits between the model name and muted metadata without borrowing
|
|
77
|
-
* a status colour, so the provider reads as its own tier even as themes change. */}
|
|
78
|
-
<span style={{ fg: theme().text, dim: true } as Record<string, unknown>}>{parts().provider}</span>
|
|
79
|
-
</Show>
|
|
80
|
-
<span style={{ fg: theme().text } as Record<string, unknown>}>{parts().model}</span>
|
|
81
|
-
</text>
|
|
82
|
-
<Show when={props.expanded}>
|
|
83
|
-
<text fg={theme().textMuted} wrapMode="none" truncate>
|
|
84
|
-
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>
|
|
85
|
-
{`${DETAIL_INDENT}AVG ${rateCell(props.row.avg)}`}
|
|
86
|
-
</span>
|
|
87
|
-
<Show when={props.row.tps !== undefined}>
|
|
88
|
-
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>{" · TPS "}</span>
|
|
89
|
-
{/* A frozen final reading stays visible but is dimmed to distinguish it from a
|
|
90
|
-
* value backed by an active stream. */}
|
|
91
|
-
<span style={{ fg: theme().textMuted, dim: !props.row.tpsLive } as Record<string, unknown>}>
|
|
92
|
-
{rateCell(props.row.tps)}
|
|
93
|
-
</span>
|
|
94
|
-
</Show>
|
|
95
|
-
</text>
|
|
96
|
-
<text fg={theme().textMuted} wrapMode="none" truncate>
|
|
97
|
-
{`${DETAIL_INDENT}TTFT ${ttftCell(props.row.ttft)} · CACHE ${cacheCell(props.row.cache)}`}
|
|
98
|
-
</text>
|
|
99
|
-
<text fg={theme().textMuted} wrapMode="none" truncate>
|
|
100
|
-
{`${DETAIL_INDENT}${props.row.cost ?? "—"}`}
|
|
101
|
-
</text>
|
|
102
|
-
</Show>
|
|
103
|
-
</box>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Per-session model panel, mounted at `sidebar_footer`. */
|
|
108
|
-
function ModelPanelView(props: {
|
|
109
|
-
api: TuiPluginApi
|
|
110
|
-
sessionID: string
|
|
111
|
-
tracker: TrackerHandle
|
|
112
|
-
revision: Accessor<number>
|
|
113
|
-
}) {
|
|
114
|
-
let mounted: string | undefined
|
|
115
|
-
const initialActive = props.tracker.internals.activeModel(props.sessionID)
|
|
116
|
-
const [expanded, setExpanded] = createSignal<Set<string>>(new Set(initialActive ? [initialActive] : []))
|
|
117
|
-
let seededActive: string | undefined = initialActive
|
|
118
|
-
let lastSession: string | undefined = props.sessionID
|
|
119
|
-
|
|
120
|
-
const panel = createMemo(() => {
|
|
121
|
-
props.revision()
|
|
122
|
-
const sessionID = props.sessionID
|
|
123
|
-
if (mounted !== sessionID) {
|
|
124
|
-
if (mounted !== undefined) props.tracker.unmountSession(mounted)
|
|
125
|
-
props.tracker.mountSession(sessionID)
|
|
126
|
-
mounted = sessionID
|
|
127
|
-
}
|
|
128
|
-
return props.tracker.modelRows(sessionID)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Fold state is in-memory only and resets per session. The active model starts expanded and
|
|
132
|
-
// every other row starts collapsed; when the active model changes we return to that default
|
|
133
|
-
// so the panel cannot accumulate open rows as models are switched.
|
|
134
|
-
createEffect(() => {
|
|
135
|
-
props.revision()
|
|
136
|
-
const sessionID = props.sessionID
|
|
137
|
-
const active = props.tracker.internals.activeModel(sessionID)
|
|
138
|
-
if (sessionID !== lastSession) {
|
|
139
|
-
lastSession = sessionID
|
|
140
|
-
seededActive = active
|
|
141
|
-
setExpanded(new Set<string>(active ? [active] : []))
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
if (active && active !== seededActive) {
|
|
145
|
-
seededActive = active
|
|
146
|
-
setExpanded(new Set<string>([active]))
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const toggle = (key: string) => {
|
|
151
|
-
setExpanded((prev) => {
|
|
152
|
-
const next = new Set(prev)
|
|
153
|
-
if (next.has(key)) next.delete(key)
|
|
154
|
-
else next.add(key)
|
|
155
|
-
return next
|
|
156
|
-
})
|
|
157
|
-
props.api.renderer?.requestRender?.()
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const controller = { toggle, isExpanded: (key: string) => expanded().has(key) }
|
|
161
|
-
setLastPanelController(controller)
|
|
162
|
-
|
|
163
|
-
onCleanup(() => {
|
|
164
|
-
if (mounted !== undefined) props.tracker.unmountSession(mounted)
|
|
165
|
-
clearLastPanelController(controller)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<box flexDirection="column">
|
|
170
|
-
<text fg={props.api.theme.current.text} wrapMode="none" truncate>
|
|
171
|
-
<b>Model-Performance · observed</b>
|
|
172
|
-
</text>
|
|
173
|
-
<Show when={panel().rows.length === 0}>
|
|
174
|
-
<text fg={props.api.theme.current.textMuted} wrapMode="none" truncate>
|
|
175
|
-
—
|
|
176
|
-
</text>
|
|
177
|
-
</Show>
|
|
178
|
-
<For each={panel().rows}>
|
|
179
|
-
{(row) => (
|
|
180
|
-
<ModelRowView api={props.api} row={row} expanded={expanded().has(row.key)} onToggle={() => toggle(row.key)} />
|
|
181
|
-
)}
|
|
182
|
-
</For>
|
|
183
|
-
<Show when={panel().overflow > 0}>
|
|
184
|
-
<text fg={props.api.theme.current.textMuted} wrapMode="none" truncate>
|
|
185
|
-
{`+${panel().overflow} more models`}
|
|
186
|
-
</text>
|
|
187
|
-
</Show>
|
|
188
|
-
<text fg={props.api.theme.current.textMuted} wrapMode="none" truncate>
|
|
189
|
-
{`Session CACHE ${panel().cache ?? "—"}${panel().retries > 0 ? ` · retries ${panel().retries}` : ""}`}
|
|
190
|
-
</text>
|
|
191
|
-
</box>
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const tui: TuiPlugin = async (api) => {
|
|
196
|
-
const [revision, setRevision] = createSignal(0)
|
|
197
|
-
const tracker = createTracker(api, () => setRevision((value) => value + 1))
|
|
198
|
-
|
|
199
|
-
api.lifecycle.onDispose(() => tracker.dispose())
|
|
200
|
-
|
|
201
|
-
api.slots.register({
|
|
202
|
-
slots: {
|
|
203
|
-
sidebar_footer(_context, props) {
|
|
204
|
-
return <ModelPanelView api={api} sessionID={props.session_id} tracker={tracker} revision={revision} />
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const plugin: TuiPluginModule = {
|
|
211
|
-
id: "opencode-tui-stats",
|
|
212
|
-
tui,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export default plugin
|