@jack-yang/opencode-tps-meter 0.1.0
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/README.md +94 -0
- package/package.json +53 -0
- package/tui.tsx +294 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# opencode-tps-meter
|
|
2
|
+
|
|
3
|
+
A TUI plugin for [opencode](https://opencode.ai) that displays real-time LLM output speed metrics next to the session prompt.
|
|
4
|
+
|
|
5
|
+
## Display
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
TPS 42.5 | AVG 38.2 | TTFT 0.8s
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **TPS** — current tokens per second (live, color-coded)
|
|
12
|
+
- **AVG** — session-wide cumulative average from completed messages (color-coded)
|
|
13
|
+
- **TTFT** — time to first token, shown immediately on first token, persists until next generation (color-coded)
|
|
14
|
+
|
|
15
|
+
## Color Tiers
|
|
16
|
+
|
|
17
|
+
**TPS & AVG** (higher is better):
|
|
18
|
+
|
|
19
|
+
| Tier | Range | Color |
|
|
20
|
+
|------|-------|-------|
|
|
21
|
+
| Slow | < 20 TPS | Red |
|
|
22
|
+
| Normal | 20–50 TPS | Yellow |
|
|
23
|
+
| Fast | 50–100 TPS | Green |
|
|
24
|
+
| Faster | > 100 TPS | Cyan |
|
|
25
|
+
|
|
26
|
+
**TTFT** (lower is better):
|
|
27
|
+
|
|
28
|
+
| Tier | Range | Color |
|
|
29
|
+
|------|-------|-------|
|
|
30
|
+
| Fast | < 0.5s | Green |
|
|
31
|
+
| OK | 0.5–2s | Yellow |
|
|
32
|
+
| Slow | > 2s | Red |
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
This plugin must be configured in `tui.json` (not `opencode.json`).
|
|
37
|
+
|
|
38
|
+
### Option 1: Local file
|
|
39
|
+
|
|
40
|
+
1. Copy `tui.tsx` to your opencode config directory:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cp tui.tsx ~/.config/opencode/tps-meter.tsx
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Add to `~/.config/opencode/tui.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"plugin": [
|
|
51
|
+
"./tps-meter.tsx"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
3. Restart opencode.
|
|
57
|
+
|
|
58
|
+
### Option 2: Project-local
|
|
59
|
+
|
|
60
|
+
Add a `.opencode/tui.json` in your project root with the same plugin path (relative to the config file location).
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
### Live TPS
|
|
65
|
+
|
|
66
|
+
Tracks text/reasoning delta events in a rolling 5-second window. Uses wall-clock duration (`now - oldest sample`) instead of inter-arrival time sums to avoid inflation from network buffering.
|
|
67
|
+
|
|
68
|
+
Token estimation uses `ceil(byteLength / 4)` with a **calibration factor**: when a message completes, the estimated token count is compared against actual `tokens.output + tokens.reasoning` from the message metadata, and a running median ratio corrects future estimates.
|
|
69
|
+
|
|
70
|
+
### AVG
|
|
71
|
+
|
|
72
|
+
Session-wide cumulative average from all completed messages: `sum(actual tokens) / sum(generation durations)`. Updates only when a message completes. Generation duration = `last delta - first delta` per message (excludes tool execution time and TTFT).
|
|
73
|
+
|
|
74
|
+
### TTFT
|
|
75
|
+
|
|
76
|
+
Time from message creation (`info.time.created`) to the first text/reasoning delta. Displayed immediately when the first token arrives, and persists until the next generation starts.
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install
|
|
82
|
+
npm test
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pure measurement functions (`measure.ts`) have 29 unit tests. The TUI plugin (`tui.tsx`) is a standalone single file (all functions inline) for direct deployment — copy it to your config directory.
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- opencode >= 1.4.3
|
|
90
|
+
- The plugin file must be placed where `tui.json` can reference it via relative path
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@jack-yang/opencode-tps-meter",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Live TPS/AVG/TTFT meter plugin for OpenCode TUI with color-coded speed tiers",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./tui": {
|
|
9
|
+
"import": "./tui.tsx"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"opencode": ">=1.4.3"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"@opencode-ai/plugin": ">=1.4.3",
|
|
17
|
+
"@opentui/core": "^0.4.2",
|
|
18
|
+
"@opentui/solid": "^0.4.2",
|
|
19
|
+
"solid-js": "^1.9.12"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"vitest": "^3.2.4",
|
|
23
|
+
"typescript": "^5.9.2"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"tui.tsx",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"keywords": [
|
|
34
|
+
"opencode",
|
|
35
|
+
"opencode-plugin",
|
|
36
|
+
"tui",
|
|
37
|
+
"tps",
|
|
38
|
+
"llm",
|
|
39
|
+
"ai"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/yangwudong/opencode-tps-meter.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/yangwudong/opencode-tps-meter#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/yangwudong/opencode-tps-meter/issues"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/tui.tsx
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TextRenderable, RGBA } from "@opentui/core"
|
|
3
|
+
import type { TuiPlugin, TuiPluginModule, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
|
+
import { onCleanup } from "solid-js"
|
|
5
|
+
|
|
6
|
+
type DeltaSample = { at: number; rawTokens: number }
|
|
7
|
+
|
|
8
|
+
type SessionStats = {
|
|
9
|
+
totalOutputTokens: number
|
|
10
|
+
totalDurationMs: number
|
|
11
|
+
calibrationRatios: number[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type TrackerState = {
|
|
15
|
+
useV2Events: boolean
|
|
16
|
+
samplesBySession: Record<string, DeltaSample[]>
|
|
17
|
+
messageCreatedAt: Record<string, number>
|
|
18
|
+
firstDeltaByMessage: Record<string, number>
|
|
19
|
+
lastDeltaByMessage: Record<string, number>
|
|
20
|
+
estimatedTokensByMessage: Record<string, number>
|
|
21
|
+
liveTtftBySession: Record<string, number>
|
|
22
|
+
activeMessageBySession: Record<string, string | undefined>
|
|
23
|
+
statsBySession: Record<string, SessionStats>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Listener = () => void
|
|
27
|
+
|
|
28
|
+
const WINDOW_MS = 5_000
|
|
29
|
+
const STALE_MS = 1_500
|
|
30
|
+
const MIN_DURATION_MS = 1_000
|
|
31
|
+
const MAX_CALIBRATION_RATIOS = 10
|
|
32
|
+
|
|
33
|
+
function estimateTokens(delta: string): number {
|
|
34
|
+
return Math.max(1, Math.ceil(Buffer.byteLength(delta, "utf8") / 4))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatTps(value: number): string | undefined {
|
|
38
|
+
if (!Number.isFinite(value) || value <= 0) return undefined
|
|
39
|
+
if (value >= 100) return Math.round(value).toString()
|
|
40
|
+
if (value >= 10) return value.toFixed(1)
|
|
41
|
+
return value.toFixed(2)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatTtft(valueMs: number): string | undefined {
|
|
45
|
+
if (!Number.isFinite(valueMs) || valueMs < 0) return undefined
|
|
46
|
+
return `${(valueMs / 1000).toFixed(1)}s`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type SpeedTier = "slow" | "normal" | "fast" | "faster"
|
|
50
|
+
function speedTier(tps: number): SpeedTier {
|
|
51
|
+
if (tps < 20) return "slow"
|
|
52
|
+
if (tps < 50) return "normal"
|
|
53
|
+
if (tps < 100) return "fast"
|
|
54
|
+
return "faster"
|
|
55
|
+
}
|
|
56
|
+
function tierColor(tier: SpeedTier, theme: TuiThemeCurrent): RGBA {
|
|
57
|
+
switch (tier) {
|
|
58
|
+
case "slow": return theme.error
|
|
59
|
+
case "normal": return theme.warning
|
|
60
|
+
case "fast": return theme.success
|
|
61
|
+
case "faster": return theme.accent
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function tpsColor(tps: number, theme: TuiThemeCurrent): RGBA {
|
|
65
|
+
return tierColor(speedTier(tps), theme)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type TtftTier = "fast" | "ok" | "slow"
|
|
69
|
+
function ttftTier(ms: number): TtftTier {
|
|
70
|
+
if (ms < 500) return "fast"
|
|
71
|
+
if (ms < 2000) return "ok"
|
|
72
|
+
return "slow"
|
|
73
|
+
}
|
|
74
|
+
function ttftColor(ms: number, theme: TuiThemeCurrent): RGBA {
|
|
75
|
+
switch (ttftTier(ms)) {
|
|
76
|
+
case "fast": return theme.success
|
|
77
|
+
case "ok": return theme.warning
|
|
78
|
+
case "slow": return theme.error
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function median(values: number[]): number {
|
|
83
|
+
if (values.length === 0) return 1.0
|
|
84
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
85
|
+
const mid = Math.floor(sorted.length / 2)
|
|
86
|
+
if (sorted.length % 2 !== 0) return sorted[mid]
|
|
87
|
+
return Math.round(((sorted[mid - 1] + sorted[mid]) / 2) * 1e10) / 1e10
|
|
88
|
+
}
|
|
89
|
+
function calibrationFactor(ratios: number[]): number {
|
|
90
|
+
if (ratios.length === 0) return 1.0
|
|
91
|
+
return median(ratios)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function calculateLiveTps(samples: DeltaSample[], now: number, calibration: number): number | undefined {
|
|
95
|
+
if (samples.length === 0) return undefined
|
|
96
|
+
const cutoff = now - WINDOW_MS
|
|
97
|
+
const relevant = samples.filter((s) => s.at >= cutoff)
|
|
98
|
+
if (relevant.length === 0) return undefined
|
|
99
|
+
const last = relevant[relevant.length - 1]
|
|
100
|
+
if (now - last.at > STALE_MS) return undefined
|
|
101
|
+
const oldest = relevant[0]
|
|
102
|
+
const durationMs = Math.max(now - oldest.at, MIN_DURATION_MS)
|
|
103
|
+
const totalRaw = relevant.reduce((sum, s) => sum + s.rawTokens, 0)
|
|
104
|
+
return (totalRaw * calibration) / (durationMs / 1000)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function recordDelta(tracker: TrackerState, sessionID: string, messageID: string, at: number, delta: string) {
|
|
108
|
+
const rawTokens = estimateTokens(delta)
|
|
109
|
+
const cutoff = at - WINDOW_MS
|
|
110
|
+
const existing = tracker.samplesBySession[sessionID] ?? []
|
|
111
|
+
tracker.samplesBySession[sessionID] = [...existing.filter((s) => s.at >= cutoff), { at, rawTokens }]
|
|
112
|
+
if (tracker.firstDeltaByMessage[messageID] === undefined) {
|
|
113
|
+
tracker.firstDeltaByMessage[messageID] = at
|
|
114
|
+
const created = tracker.messageCreatedAt[messageID]
|
|
115
|
+
if (created !== undefined) {
|
|
116
|
+
tracker.liveTtftBySession[sessionID] = Math.max(0, at - created)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
tracker.lastDeltaByMessage[messageID] = at
|
|
120
|
+
tracker.estimatedTokensByMessage[messageID] = (tracker.estimatedTokensByMessage[messageID] ?? 0) + rawTokens
|
|
121
|
+
tracker.activeMessageBySession[sessionID] = messageID
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function MeterDisplay(props: {
|
|
125
|
+
api: Parameters<TuiPlugin>[0]
|
|
126
|
+
sessionID: string
|
|
127
|
+
tracker: TrackerState
|
|
128
|
+
subscribe: (listener: Listener) => () => void
|
|
129
|
+
}) {
|
|
130
|
+
let tpsRef: TextRenderable | undefined
|
|
131
|
+
let avgRef: TextRenderable | undefined
|
|
132
|
+
let ttftRef: TextRenderable | undefined
|
|
133
|
+
const theme = props.api.theme.current
|
|
134
|
+
const muted = theme.textMuted
|
|
135
|
+
|
|
136
|
+
const sync = () => {
|
|
137
|
+
const stats = props.tracker.statsBySession[props.sessionID]
|
|
138
|
+
const calibration = calibrationFactor(stats?.calibrationRatios ?? [])
|
|
139
|
+
const samples = props.tracker.samplesBySession[props.sessionID] ?? []
|
|
140
|
+
const now = Date.now()
|
|
141
|
+
const live = calculateLiveTps(samples, now, calibration)
|
|
142
|
+
const avg = stats && stats.totalDurationMs > 0 ? stats.totalOutputTokens / (stats.totalDurationMs / 1000) : undefined
|
|
143
|
+
const ttft = props.tracker.liveTtftBySession[props.sessionID]
|
|
144
|
+
|
|
145
|
+
if (tpsRef) {
|
|
146
|
+
tpsRef.content = live !== undefined ? formatTps(live) ?? "--" : "--"
|
|
147
|
+
tpsRef.fg = live !== undefined ? tpsColor(live, theme) : muted
|
|
148
|
+
}
|
|
149
|
+
if (avgRef) {
|
|
150
|
+
avgRef.content = avg !== undefined ? formatTps(avg) ?? "--" : "--"
|
|
151
|
+
avgRef.fg = avg !== undefined ? tpsColor(avg, theme) : muted
|
|
152
|
+
}
|
|
153
|
+
if (ttftRef) {
|
|
154
|
+
ttftRef.content = ttft !== undefined ? formatTtft(ttft) ?? "--" : "--"
|
|
155
|
+
ttftRef.fg = ttft !== undefined ? ttftColor(ttft, theme) : muted
|
|
156
|
+
}
|
|
157
|
+
props.api.renderer.requestRender()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const unsubscribe = props.subscribe(sync)
|
|
161
|
+
onCleanup(unsubscribe)
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<box flexDirection="row">
|
|
165
|
+
<text fg={muted}>TPS </text>
|
|
166
|
+
<text ref={(el: TextRenderable) => { tpsRef = el; sync() }} fg={muted}>--</text>
|
|
167
|
+
<text fg={muted}> | AVG </text>
|
|
168
|
+
<text ref={(el: TextRenderable) => { avgRef = el; sync() }} fg={muted}>--</text>
|
|
169
|
+
<text fg={muted}> | TTFT </text>
|
|
170
|
+
<text ref={(el: TextRenderable) => { ttftRef = el; sync() }} fg={muted}>--</text>
|
|
171
|
+
</box>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const tui: TuiPlugin = async (api) => {
|
|
176
|
+
const tracker: TrackerState = {
|
|
177
|
+
useV2Events: false,
|
|
178
|
+
samplesBySession: {},
|
|
179
|
+
messageCreatedAt: {},
|
|
180
|
+
firstDeltaByMessage: {},
|
|
181
|
+
lastDeltaByMessage: {},
|
|
182
|
+
estimatedTokensByMessage: {},
|
|
183
|
+
liveTtftBySession: {},
|
|
184
|
+
activeMessageBySession: {},
|
|
185
|
+
statsBySession: {},
|
|
186
|
+
}
|
|
187
|
+
const listeners = new Set<Listener>()
|
|
188
|
+
const bump = () => { for (const l of listeners) l() }
|
|
189
|
+
const subscribe = (listener: Listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }
|
|
190
|
+
|
|
191
|
+
const onTextDelta = api.event.on("session.next.text.delta", (evt) => {
|
|
192
|
+
tracker.useV2Events = true
|
|
193
|
+
recordDelta(tracker, evt.properties.sessionID, evt.properties.assistantMessageID, evt.properties.timestamp, evt.properties.delta)
|
|
194
|
+
bump()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const onReasoningDelta = api.event.on("session.next.reasoning.delta", (evt) => {
|
|
198
|
+
tracker.useV2Events = true
|
|
199
|
+
recordDelta(tracker, evt.properties.sessionID, evt.properties.assistantMessageID, evt.properties.timestamp, evt.properties.delta)
|
|
200
|
+
bump()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const onPartDelta = api.event.on("message.part.delta", (evt) => {
|
|
204
|
+
if (tracker.useV2Events) return
|
|
205
|
+
if (evt.properties.field !== "text") return
|
|
206
|
+
const parts = api.state.part(evt.properties.messageID)
|
|
207
|
+
const part = parts.find((p) => p.id === evt.properties.partID)
|
|
208
|
+
if (!part || (part.type !== "text" && part.type !== "reasoning")) return
|
|
209
|
+
recordDelta(tracker, evt.properties.sessionID, evt.properties.messageID, Date.now(), evt.properties.delta)
|
|
210
|
+
bump()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const onMessageUpdated = api.event.on("message.updated", (evt) => {
|
|
214
|
+
const info = evt.properties.info
|
|
215
|
+
if (info.role !== "assistant") return
|
|
216
|
+
|
|
217
|
+
if (!info.time.completed) {
|
|
218
|
+
tracker.messageCreatedAt[info.id] = info.time.created
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const sessionID = info.sessionID ?? evt.properties.sessionID
|
|
223
|
+
if (!tracker.statsBySession[sessionID]) {
|
|
224
|
+
tracker.statsBySession[sessionID] = { totalOutputTokens: 0, totalDurationMs: 0, calibrationRatios: [] }
|
|
225
|
+
}
|
|
226
|
+
const stats = tracker.statsBySession[sessionID]!
|
|
227
|
+
|
|
228
|
+
const actualTokens = info.tokens.output + info.tokens.reasoning
|
|
229
|
+
const firstDelta = tracker.firstDeltaByMessage[info.id]
|
|
230
|
+
const lastDelta = tracker.lastDeltaByMessage[info.id]
|
|
231
|
+
const generationMs = firstDelta !== undefined && lastDelta !== undefined
|
|
232
|
+
? Math.max(lastDelta - firstDelta, MIN_DURATION_MS) : 0
|
|
233
|
+
|
|
234
|
+
if (actualTokens > 0 && generationMs > 0) {
|
|
235
|
+
stats.totalOutputTokens += actualTokens
|
|
236
|
+
stats.totalDurationMs += generationMs
|
|
237
|
+
|
|
238
|
+
const estimated = tracker.estimatedTokensByMessage[info.id] ?? 0
|
|
239
|
+
if (estimated > 0) {
|
|
240
|
+
const ratio = Math.min(Math.max(actualTokens / estimated, 0.3), 3.0)
|
|
241
|
+
stats.calibrationRatios = [...stats.calibrationRatios, ratio].slice(-MAX_CALIBRATION_RATIOS)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
delete tracker.estimatedTokensByMessage[info.id]
|
|
246
|
+
delete tracker.firstDeltaByMessage[info.id]
|
|
247
|
+
delete tracker.lastDeltaByMessage[info.id]
|
|
248
|
+
delete tracker.messageCreatedAt[info.id]
|
|
249
|
+
delete tracker.activeMessageBySession[sessionID]
|
|
250
|
+
delete tracker.samplesBySession[sessionID]
|
|
251
|
+
bump()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const onToolInputStarted = api.event.on("session.next.tool.input.started", (evt) => {
|
|
255
|
+
delete tracker.samplesBySession[evt.properties.sessionID]
|
|
256
|
+
bump()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const onPartUpdated = api.event.on("message.part.updated", (evt) => {
|
|
260
|
+
if (evt.properties.part.type !== "tool") return
|
|
261
|
+
if (["running", "completed", "error"].includes(evt.properties.part.state.status)) {
|
|
262
|
+
delete tracker.samplesBySession[evt.properties.part.sessionID ?? evt.properties.sessionID]
|
|
263
|
+
bump()
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const timer = setInterval(() => {
|
|
268
|
+
const now = Date.now()
|
|
269
|
+
const cutoff = now - WINDOW_MS
|
|
270
|
+
for (const [sid, samples] of Object.entries(tracker.samplesBySession)) {
|
|
271
|
+
const pruned = samples.filter((s) => s.at >= cutoff)
|
|
272
|
+
if (pruned.length > 0) tracker.samplesBySession[sid] = pruned
|
|
273
|
+
else delete tracker.samplesBySession[sid]
|
|
274
|
+
}
|
|
275
|
+
bump()
|
|
276
|
+
}, 1000)
|
|
277
|
+
|
|
278
|
+
api.lifecycle.onDispose(() => {
|
|
279
|
+
onTextDelta(); onReasoningDelta(); onPartDelta()
|
|
280
|
+
onMessageUpdated(); onToolInputStarted(); onPartUpdated()
|
|
281
|
+
clearInterval(timer)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
api.slots.register({
|
|
285
|
+
slots: {
|
|
286
|
+
session_prompt_right(_ctx, value) {
|
|
287
|
+
return <MeterDisplay api={api} sessionID={value.session_id} tracker={tracker} subscribe={subscribe} />
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const plugin: TuiPluginModule & { id: string } = { id: "opencode-tps-meter", tui }
|
|
294
|
+
export default plugin
|