opencode-tui-stats 0.4.1 → 0.4.2
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.en.md +17 -3
- package/README.md +17 -3
- package/assets/demo.gif +0 -0
- package/package.json +1 -1
- package/tracker.ts +25 -9
- package/tui.tsx +19 -7
package/README.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-tui-stats
|
|
2
2
|
|
|
3
|
-
A TUI plugin for OpenCode 1.x (v1)
|
|
3
|
+
A TUI plugin for OpenCode 1.x (v1), mounted at the bottom of the right sidebar (slot `sidebar_footer`), with the panel title `Model-Performance · observed`. Each model occupies one row that can be collapsed/expanded by mouse click.
|
|
4
4
|
|
|
5
5
|
## Demo
|
|
6
6
|
|
|
@@ -18,11 +18,25 @@ This command writes the plugin into opencode's TUI config. You can also install
|
|
|
18
18
|
|
|
19
19
|
- opencode ≥ 1.3.14
|
|
20
20
|
|
|
21
|
+
## Collapsing & subagents
|
|
22
|
+
|
|
23
|
+
- Each model occupies one row that can be collapsed/expanded by mouse click (`▼`/`▶`); rows are collapsed by default, and the most recently used model starts expanded.
|
|
24
|
+
- Collapsed: `▶ [↳ ]provider/model` (redundant same-provider prefix omitted; provider dimmed, model name bright).
|
|
25
|
+
- Expanded:
|
|
26
|
+
- `AVG …t/s · TPS …t/s` (TPS shown only when a value exists; the final value is kept and dimmed after generation ends)
|
|
27
|
+
- `TTFT …s · CACHE …%`
|
|
28
|
+
- `$…` (cost on its own line)
|
|
29
|
+
- Models from subagents (sub-sessions) are merged into the same panel; rows originating from a subagent are prefixed with `↳`, and the same model (parent/child) is merged and summed.
|
|
30
|
+
- Panel footer: `Session CACHE …%` (aggregated across the whole session + subagent tree, 1 decimal place); `retries` is shown only when >0.
|
|
31
|
+
|
|
21
32
|
## Metrics
|
|
22
33
|
|
|
23
|
-
- `TPS`:
|
|
34
|
+
- `TPS`: generation rate within the most recent ~5-second streaming window (tokens/sec); the final value is frozen and dimmed after the stream stops.
|
|
24
35
|
- `AVG`: average generation rate of completed messages in this session (tokens/sec).
|
|
25
|
-
- `TTFT`: average time to first token
|
|
36
|
+
- `TTFT`: average time to first token (seconds, approximate, includes queue/retry wait).
|
|
37
|
+
- `CACHE`: prompt cache hit rate = `cache.read / (input + cache.read + cache.write)`, 1 decimal place.
|
|
38
|
+
- `COST`: cumulative session cost (aggregated per model/session).
|
|
39
|
+
- `RETRY`: retry count, derived from the attempt reported by the provider.
|
|
26
40
|
|
|
27
41
|
## License
|
|
28
42
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-tui-stats
|
|
2
2
|
|
|
3
|
-
OpenCode 1.x(v1)的 TUI
|
|
3
|
+
OpenCode 1.x(v1)的 TUI 插件,安装在右侧栏底部(slot `sidebar_footer`),面板标题为 `Model-Performance · observed`。每个模型占一行,鼠标点击可折叠/展开。
|
|
4
4
|
|
|
5
5
|
## 演示
|
|
6
6
|
|
|
@@ -18,11 +18,25 @@ opencode plugin opencode-tui-stats --global
|
|
|
18
18
|
|
|
19
19
|
- opencode ≥ 1.3.14
|
|
20
20
|
|
|
21
|
+
## 折叠与子会话
|
|
22
|
+
|
|
23
|
+
- 每个模型占一行,鼠标点击可折叠/展开(`▼`/`▶`);默认折叠,最近使用的模型默认展开。
|
|
24
|
+
- 折叠态:`▶ [↳ ]provider/model`(同 provider 前缀省略;provider 暗色、模型名亮色)。
|
|
25
|
+
- 展开态:
|
|
26
|
+
- `AVG …t/s · TPS …t/s`(TPS 仅在有值时显示;生成结束后保留最后值并变暗)
|
|
27
|
+
- `TTFT …s · CACHE …%`
|
|
28
|
+
- `$…`(成本单独一行)
|
|
29
|
+
- 子会话(子 agent)的模型会并入同一面板,来自子会话的行前缀 `↳`;同一模型(父/子)合并求和。
|
|
30
|
+
- 面板底部:`Session CACHE …%`(整个会话 + 子会话树聚合,1 位小数);`retries` 仅在 >0 时显示。
|
|
31
|
+
|
|
21
32
|
## 指标说明
|
|
22
33
|
|
|
23
|
-
- `TPS`:最近约 5
|
|
34
|
+
- `TPS`:最近约 5 秒流式窗口内的生成速度(token/秒);停止后冻结最后值并变暗。
|
|
24
35
|
- `AVG`:本会话已完成消息的平均生成速度(token/秒)。
|
|
25
|
-
- `TTFT
|
|
36
|
+
- `TTFT`:平均首字延迟(秒,近似值,包含排队/重试等待)。
|
|
37
|
+
- `CACHE`:提示词缓存命中率 = `cache.read / (input + cache.read + cache.write)`,1 位小数。
|
|
38
|
+
- `COST`:会话累计成本(按模型/会话聚合)。
|
|
39
|
+
- `RETRY`:按 provider 上报的 attempt 统计的重试次数。
|
|
26
40
|
|
|
27
41
|
## License
|
|
28
42
|
|
package/assets/demo.gif
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-tui-stats",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"description": "OpenCode 1 TUI plugin that shows live TPS, average TPS, and average time to first token in the session prompt.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
package/tracker.ts
CHANGED
|
@@ -53,6 +53,9 @@ export type SessionModels = {
|
|
|
53
53
|
aggregates: Record<string, ModelAggregate>
|
|
54
54
|
/** `messageID -> providerID/modelID`, used to attribute live samples to a model. */
|
|
55
55
|
messageModels: Map<string, string>
|
|
56
|
+
/** Last measured live rate per model key, kept after the stream goes idle/stale so the
|
|
57
|
+
* final reading stays visible instead of disappearing. */
|
|
58
|
+
lastRate: Record<string, string>
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
export type Tracker = {
|
|
@@ -182,7 +185,10 @@ export type ModelRow = {
|
|
|
182
185
|
active: boolean
|
|
183
186
|
/** True when any descendant (subagent) session contributed data to this row. */
|
|
184
187
|
subagent?: boolean
|
|
188
|
+
/** Last measured live rate, or `undefined` when this model has never streamed. */
|
|
185
189
|
tps?: string
|
|
190
|
+
/** True while `tps` is backed by an active stream; false for a frozen final value. */
|
|
191
|
+
tpsLive: boolean
|
|
186
192
|
avg?: string
|
|
187
193
|
ttft?: string
|
|
188
194
|
cache?: string
|
|
@@ -323,10 +329,13 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
323
329
|
bump()
|
|
324
330
|
}
|
|
325
331
|
|
|
326
|
-
/** Live rate restricted to samples attributed to `key` via the `messageID -> model` mapping.
|
|
327
|
-
|
|
332
|
+
/** Live rate restricted to samples attributed to `key` via the `messageID -> model` mapping.
|
|
333
|
+
* When the session is idle or the samples are stale, the last measured value is frozen and
|
|
334
|
+
* returned with `live: false` instead of disappearing. */
|
|
335
|
+
const sessionLiveRate = (sessionID: string, key: string): { value?: string; live: boolean } => {
|
|
336
|
+
const frozen = { value: tracker.models[sessionID]?.lastRate[key], live: false }
|
|
328
337
|
const status = api.state.session.status(sessionID)
|
|
329
|
-
if (!status || status.type === "idle") return
|
|
338
|
+
if (!status || status.type === "idle") return frozen
|
|
330
339
|
|
|
331
340
|
const now = Date.now()
|
|
332
341
|
const mapping = tracker.models[sessionID]?.messageModels
|
|
@@ -334,10 +343,14 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
334
343
|
(sample) => now - sample.at <= STREAM_WINDOW_MS && mapping?.get(sample.messageID) === key,
|
|
335
344
|
)
|
|
336
345
|
const last = samples[samples.length - 1]
|
|
337
|
-
if (!last || now - last.at > LIVE_STALE_MS) return
|
|
346
|
+
if (!last || now - last.at > LIVE_STALE_MS) return frozen
|
|
338
347
|
|
|
339
348
|
const tokens = samples.reduce((sum, sample) => sum + sample.tokens, 0)
|
|
340
|
-
|
|
349
|
+
const value = formatRate(tokens / (activeDuration(samples, now) / 1_000))
|
|
350
|
+
if (value === undefined) return frozen
|
|
351
|
+
const models = tracker.models[sessionID]
|
|
352
|
+
if (models) models.lastRate[key] = value
|
|
353
|
+
return { value, live: true }
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
/** Fold a session's parent from `session.created`/`session.updated`, or lazily from state.
|
|
@@ -414,14 +427,14 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
414
427
|
/** Live rate for a merged row: the parent and its subagents share one row, so only the
|
|
415
428
|
* session/model pair that most recently saw activity contributes. Concurrent streams in a
|
|
416
429
|
* parent and a child are therefore approximated by the newer one instead of being summed. */
|
|
417
|
-
const liveRateFor = (sessions: string[], key: string) => {
|
|
430
|
+
const liveRateFor = (sessions: string[], key: string): { value?: string; live: boolean } => {
|
|
418
431
|
let latest: { sessionID: string; at: number } | undefined
|
|
419
432
|
for (const sessionID of sessions) {
|
|
420
433
|
const aggregate = tracker.models[sessionID]?.aggregates[key]
|
|
421
434
|
if (!aggregate) continue
|
|
422
435
|
if (!latest || aggregate.lastAt >= latest.at) latest = { sessionID, at: aggregate.lastAt }
|
|
423
436
|
}
|
|
424
|
-
if (!latest) return
|
|
437
|
+
if (!latest) return { live: false }
|
|
425
438
|
return sessionLiveRate(latest.sessionID, key)
|
|
426
439
|
}
|
|
427
440
|
|
|
@@ -482,12 +495,14 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
482
495
|
const rows = visible.map<ModelRow>((key) => {
|
|
483
496
|
const aggregate = aggregates[key]
|
|
484
497
|
const isActive = key === active
|
|
498
|
+
const tps = isActive ? liveRateFor(subtree, key) : { value: undefined, live: false }
|
|
485
499
|
return {
|
|
486
500
|
key,
|
|
487
501
|
label: key,
|
|
488
502
|
active: isActive,
|
|
489
503
|
subagent: subagentModels.has(key),
|
|
490
|
-
tps:
|
|
504
|
+
tps: tps.value,
|
|
505
|
+
tpsLive: tps.live,
|
|
491
506
|
avg: averageRate(aggregate),
|
|
492
507
|
ttft: averageTtft(aggregate),
|
|
493
508
|
cache: cacheText(aggregate),
|
|
@@ -533,7 +548,8 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
533
548
|
// Observe the model as soon as a message appears so the sidebar can attribute live samples
|
|
534
549
|
// and mark the most recently used model as active.
|
|
535
550
|
const models =
|
|
536
|
-
tracker.models[sessionID] ??
|
|
551
|
+
tracker.models[sessionID] ??
|
|
552
|
+
(tracker.models[sessionID] = { order: [], aggregates: {}, messageModels: new Map(), lastRate: {} })
|
|
537
553
|
touchModel(models, key)
|
|
538
554
|
models.active = key
|
|
539
555
|
models.aggregates[key] = { ...models.aggregates[key], lastAt: Date.now() }
|
package/tui.tsx
CHANGED
|
@@ -65,6 +65,9 @@ function ModelRowView(props: { api: TuiPluginApi; row: ModelRow; expanded: boole
|
|
|
65
65
|
return (
|
|
66
66
|
<box flexDirection="column" onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
|
|
67
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>
|
|
68
71
|
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>{`${props.expanded ? "▼" : "▶"} `}</span>
|
|
69
72
|
<Show when={props.row.subagent}>
|
|
70
73
|
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>{"↳ "}</span>
|
|
@@ -78,14 +81,23 @@ function ModelRowView(props: { api: TuiPluginApi; row: ModelRow; expanded: boole
|
|
|
78
81
|
</text>
|
|
79
82
|
<Show when={props.expanded}>
|
|
80
83
|
<text fg={theme().textMuted} wrapMode="none" truncate>
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
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>
|
|
84
95
|
</text>
|
|
85
96
|
<text fg={theme().textMuted} wrapMode="none" truncate>
|
|
86
|
-
{`${DETAIL_INDENT}TTFT ${ttftCell(props.row.ttft)} · CACHE ${cacheCell(props.row.cache)}
|
|
87
|
-
|
|
88
|
-
|
|
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 ?? "—"}`}
|
|
89
101
|
</text>
|
|
90
102
|
</Show>
|
|
91
103
|
</box>
|
|
@@ -156,7 +168,7 @@ function ModelPanelView(props: {
|
|
|
156
168
|
return (
|
|
157
169
|
<box flexDirection="column">
|
|
158
170
|
<text fg={props.api.theme.current.text} wrapMode="none" truncate>
|
|
159
|
-
<b>Performance · observed</b>
|
|
171
|
+
<b>Model-Performance · observed</b>
|
|
160
172
|
</text>
|
|
161
173
|
<Show when={panel().rows.length === 0}>
|
|
162
174
|
<text fg={props.api.theme.current.textMuted} wrapMode="none" truncate>
|