@tinkink/oc-tps 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tui.tsx +67 -31
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@tinkink/oc-tps",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "Temporary OpenCode 2 fork of Tarquinen/oc-tps: live TPS, average TPS, and average TTFT in the session prompt.",
6
6
  "repository": {
7
7
  "type": "git",
package/tui.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import { Plugin } from "@opencode/plugin/tui"
3
- import { createMemo, createSignal, type Accessor } from "solid-js"
3
+ import { onCleanup } from "solid-js"
4
+ import type { TextRenderable } from "@opentui/core"
4
5
 
5
6
  type StreamSample = {
6
7
  at: number
@@ -63,42 +64,63 @@ function activeDuration(samples: StreamSample[], tailAt?: number) {
63
64
  return Math.max(duration, SINGLE_SAMPLE_MS)
64
65
  }
65
66
 
67
+ function statusText(context: Plugin.Context, sessionID: string, tracker: Tracker) {
68
+ const totals = tracker.averages[sessionID]
69
+ const average = totals ? formatRate(totals.totalTokens / (totals.totalDurationMs / 1_000)) : undefined
70
+ const ttft = totals?.stepCount ? formatTtft(totals.totalTtftMs / totals.stepCount / 1_000) : undefined
71
+
72
+ let live: string | undefined
73
+ if (context.data.session.status(sessionID) === "running") {
74
+ const now = Date.now()
75
+ const samples = (tracker.samples[sessionID] ?? []).filter((sample) => now - sample.at <= STREAM_WINDOW_MS)
76
+ const last = samples[samples.length - 1]
77
+ if (last && now - last.at <= LIVE_STALE_MS) {
78
+ const tokens = samples.reduce((sum, sample) => sum + sample.tokens, 0)
79
+ live = formatRate(tokens / (activeDuration(samples, now) / 1_000))
80
+ }
81
+ }
82
+
83
+ return `TPS ${live ?? "-"} | AVG ${average ?? "-"} | TTFT ${ttft ?? "-"}`
84
+ }
85
+
86
+ /**
87
+ * Renders into `prompt.footer` by writing the text renderable directly.
88
+ *
89
+ * A published CLI plugin is installed under `node_modules`, where OpenCode does
90
+ * not inject its own `solid-js`/`@opentui/solid` runtime. The plugin therefore
91
+ * runs against its own Solid copy, so signal/memo updates never reach the host
92
+ * render loop and the footer would stay frozen at its first value. Driving the
93
+ * renderable and calling `requestRender()` keeps the display live regardless of
94
+ * which Solid instance the plugin was loaded with.
95
+ */
66
96
  function Status(props: {
67
97
  context: Plugin.Context
68
98
  sessionID: string
69
99
  tracker: Tracker
70
- revision: Accessor<number>
100
+ register: (paint: () => void) => () => void
71
101
  }) {
72
- const content = createMemo(() => {
73
- props.revision()
74
- const totals = props.tracker.averages[props.sessionID]
75
- const average = totals
76
- ? formatRate(totals.totalTokens / (totals.totalDurationMs / 1_000))
77
- : undefined
78
- const ttft = totals?.stepCount
79
- ? formatTtft(totals.totalTtftMs / totals.stepCount / 1_000)
80
- : undefined
81
-
82
- let live: string | undefined
83
- if (props.context.data.session.status(props.sessionID) === "running") {
84
- const now = Date.now()
85
- const samples = (props.tracker.samples[props.sessionID] ?? []).filter(
86
- (sample) => now - sample.at <= STREAM_WINDOW_MS,
87
- )
88
- const last = samples[samples.length - 1]
89
- if (last && now - last.at <= LIVE_STALE_MS) {
90
- const tokens = samples.reduce((sum, sample) => sum + sample.tokens, 0)
91
- live = formatRate(tokens / (activeDuration(samples, now) / 1_000))
92
- }
93
- }
102
+ let text: TextRenderable | undefined
103
+
104
+ const paint = () => {
105
+ if (!text) return
106
+ text.content = statusText(props.context, props.sessionID, props.tracker)
107
+ props.context.renderer.requestRender()
108
+ }
94
109
 
95
- return `TPS ${live ?? "-"} | AVG ${average ?? "-"} | TTFT ${ttft ?? "-"}`
96
- })
110
+ const unregister = props.register(paint)
111
+ onCleanup(unregister)
97
112
 
98
113
  return (
99
114
  <box position="absolute" right={2} bottom={2} height={1} flexDirection="row">
100
- <text fg={props.context.theme.text.muted} flexShrink={0}>
101
- {content()}
115
+ <text
116
+ ref={(element) => {
117
+ text = element as unknown as TextRenderable
118
+ paint()
119
+ }}
120
+ fg={props.context.theme.text.muted}
121
+ flexShrink={0}
122
+ >
123
+ {statusText(props.context, props.sessionID, props.tracker)}
102
124
  </text>
103
125
  </box>
104
126
  )
@@ -113,8 +135,15 @@ export default Plugin.define({
113
135
  timings: {},
114
136
  averages: {},
115
137
  }
116
- const [revision, setRevision] = createSignal(0)
117
- const bump = () => setRevision((value) => value + 1)
138
+
139
+ let painter: (() => void) | undefined
140
+ const bump = () => painter?.()
141
+ const register = (paint: () => void) => {
142
+ painter = paint
143
+ return () => {
144
+ if (painter === paint) painter = undefined
145
+ }
146
+ }
118
147
 
119
148
  const clearLive = (sessionID: string) => {
120
149
  if (!tracker.samples[sessionID]) return
@@ -221,7 +250,14 @@ export default Plugin.define({
221
250
  append: "prompt.footer",
222
251
  render: (props) => {
223
252
  if (!props.sessionID || props.mode !== "normal") return null
224
- return <Status context={context} sessionID={props.sessionID} tracker={tracker} revision={revision} />
253
+ return (
254
+ <Status
255
+ context={context}
256
+ sessionID={props.sessionID}
257
+ tracker={tracker}
258
+ register={register}
259
+ />
260
+ )
225
261
  },
226
262
  })
227
263