opencode-tui-stats 0.4.2 → 0.4.4

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/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