opencode2-tps 0.1.1 → 0.2.0-rc.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.
- package/README.md +3 -3
- package/dist/tui.js +161 -95
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ When the run ends, the number freezes at the run average and stays there until t
|
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
|
-
Built against the OpenCode 2 preview. The earliest known compatible beta is `0.0.0-beta-17595`; the latest tested beta is `0.0.0-beta-
|
|
19
|
+
Built against the OpenCode 2 preview. The earliest known compatible beta is `0.0.0-beta-17595`; the latest tested beta is `0.0.0-beta-17639`. The TUI plugin API is still moving, so a much newer or older build may drop the indicator without an error: a renamed event stops arriving, and an unknown composer slot gets quietly rerouted. If the figure never appears, check your CLI version first.
|
|
20
20
|
|
|
21
21
|
Add the package to `~/.config/opencode/cli.json`:
|
|
22
22
|
|
|
@@ -69,9 +69,9 @@ The defaults are usable as they are. For the full option list, the ranges and mo
|
|
|
69
69
|
|
|
70
70
|
## How it works
|
|
71
71
|
|
|
72
|
-
The plugin counts the output bytes of a run and estimates tokens from the byte total.
|
|
72
|
+
The plugin counts the output bytes of a run and estimates tokens from the byte total. When the host reports the session's real token counts, the plugin measures its own bytes-per-token ratio for that session and switches to it — until then it assumes 4.75 bytes per token. TPS is the run's estimated tokens divided by active model-generation time: timing starts at each step's first output and continues without pause for provider stalls, while tool-execution intervals and time between model steps are excluded.
|
|
73
73
|
|
|
74
|
-
The
|
|
74
|
+
The measured ratio depends on the model and is calibrated once per session. Changing models mid-session can therefore make the token and t/s estimates inaccurate; start a new session after switching models for a fresh calibration. This also avoids the prompt-cache disruption associated with changing models in existing sessions.
|
|
75
75
|
|
|
76
76
|
For more detail, see [Architecture](docs/development.md#architecture).
|
|
77
77
|
|
package/dist/tui.js
CHANGED
|
@@ -87,19 +87,22 @@ function mark(line) {
|
|
|
87
87
|
// ---------------------------------------------------------------------------
|
|
88
88
|
// tuning
|
|
89
89
|
|
|
90
|
-
const BYTES_PER_TOKEN = 5;
|
|
91
90
|
export const DEFAULT_CONFIG = {
|
|
92
|
-
|
|
93
|
-
liveStaleMs: 1_500,
|
|
94
|
-
singleSampleMinMs: 250,
|
|
95
|
-
singleSampleMaxMs: 1_000,
|
|
96
|
-
tailMaxMs: 1_000,
|
|
97
|
-
gapCapMs: 2_000
|
|
91
|
+
bytesPerToken: 4.75
|
|
98
92
|
};
|
|
99
93
|
// The frozen final average stays visible until the next prompt starts a new run.
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
// Step completion events carry the generated-token count for that model call.
|
|
96
|
+
// Pairing it with deltas from the same assistant message avoids contamination
|
|
97
|
+
// from concurrent title generation and other session-level usage updates.
|
|
98
|
+
export const CALIBRATION_MIN_BYTES = 2_048;
|
|
99
|
+
export const CALIBRATION_MIN_TOKENS = 50;
|
|
100
|
+
// Bounds shared by the option clamp and the calibration sanity check: a ratio
|
|
101
|
+
// outside them means the report does not describe the bytes the plugin watched.
|
|
102
|
+
const BYTES_PER_TOKEN_MIN = 1;
|
|
103
|
+
const BYTES_PER_TOKEN_MAX = 16;
|
|
104
|
+
function estimateTokens(bytes, bytesPerToken) {
|
|
105
|
+
return Math.ceil(bytes / bytesPerToken);
|
|
103
106
|
}
|
|
104
107
|
function formatTps(value) {
|
|
105
108
|
if (value < 10) return value.toFixed(2);
|
|
@@ -120,6 +123,10 @@ export class TpsTracker {
|
|
|
120
123
|
// Insertion order is kept equal to run-start recency (see beginRun), which is
|
|
121
124
|
// what makes eviction from the front drop the stalest session.
|
|
122
125
|
runs = new Map();
|
|
126
|
+
// Calibrated bytes/token and the currently observed model step. Both share
|
|
127
|
+
// the bounded lifecycle of `runs`.
|
|
128
|
+
ratios = new Map();
|
|
129
|
+
calibrationSteps = new Map();
|
|
123
130
|
constructor(config = DEFAULT_CONFIG) {
|
|
124
131
|
this.config = config;
|
|
125
132
|
}
|
|
@@ -128,10 +135,13 @@ export class TpsTracker {
|
|
|
128
135
|
if (!st) {
|
|
129
136
|
st = {
|
|
130
137
|
phase: "ended",
|
|
131
|
-
samples: [],
|
|
132
138
|
bytes: 0,
|
|
133
139
|
activeMs: 0,
|
|
140
|
+
activeAssistantMessageID: null,
|
|
134
141
|
lastSampleAt: null,
|
|
142
|
+
activeTools: new Set(),
|
|
143
|
+
toolPauseStartedAt: null,
|
|
144
|
+
toolPausedMs: 0,
|
|
135
145
|
frozen: null
|
|
136
146
|
};
|
|
137
147
|
this.runs.set(sessionID, st);
|
|
@@ -140,11 +150,15 @@ export class TpsTracker {
|
|
|
140
150
|
}
|
|
141
151
|
beginRun(sessionID) {
|
|
142
152
|
const st = this.state(sessionID);
|
|
153
|
+
this.calibrationSteps.delete(sessionID);
|
|
143
154
|
st.phase = "running";
|
|
144
|
-
st.samples = [];
|
|
145
155
|
st.bytes = 0;
|
|
146
156
|
st.activeMs = 0;
|
|
157
|
+
st.activeAssistantMessageID = null;
|
|
147
158
|
st.lastSampleAt = null;
|
|
159
|
+
st.activeTools.clear();
|
|
160
|
+
st.toolPauseStartedAt = null;
|
|
161
|
+
st.toolPausedMs = 0;
|
|
148
162
|
st.frozen = null;
|
|
149
163
|
// Re-insert so this session becomes the newest in iteration order. Every
|
|
150
164
|
// entry is created through here, so the cap is checked on the one path that
|
|
@@ -158,10 +172,18 @@ export class TpsTracker {
|
|
|
158
172
|
for (const [sessionID, st] of this.runs) {
|
|
159
173
|
if (this.runs.size <= MAX_TRACKED_RUNS) return;
|
|
160
174
|
if (st.phase === "running") continue;
|
|
161
|
-
this.
|
|
175
|
+
this.dropSession(sessionID);
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
|
-
|
|
178
|
+
advanceTiming(st, now) {
|
|
179
|
+
if (st.lastSampleAt === null) return;
|
|
180
|
+
const currentPause = st.toolPauseStartedAt === null ? 0 : Math.max(0, now - st.toolPauseStartedAt);
|
|
181
|
+
st.activeMs += Math.max(0, now - st.lastSampleAt - st.toolPausedMs - currentPause);
|
|
182
|
+
st.lastSampleAt = now;
|
|
183
|
+
st.toolPausedMs = 0;
|
|
184
|
+
if (st.toolPauseStartedAt !== null) st.toolPauseStartedAt = now;
|
|
185
|
+
}
|
|
186
|
+
push(sessionID, delta, now, assistantMessageID) {
|
|
165
187
|
if (!delta) return;
|
|
166
188
|
const st = this.state(sessionID);
|
|
167
189
|
if (st.phase !== "running") this.beginRun(sessionID); // execution.started missed
|
|
@@ -169,72 +191,124 @@ export class TpsTracker {
|
|
|
169
191
|
// Bytes accumulate; tokens are derived from the run total, so providers
|
|
170
192
|
// that emit 1-3 byte deltas are not rounded up on every single one.
|
|
171
193
|
const bytes = Buffer.byteLength(delta, "utf8");
|
|
172
|
-
st.samples.push({
|
|
173
|
-
bytes,
|
|
174
|
-
timestamp: now
|
|
175
|
-
});
|
|
176
194
|
st.bytes += bytes;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
st.
|
|
195
|
+
const calibration = this.calibrationSteps.get(sessionID);
|
|
196
|
+
if (calibration && calibration.assistantMessageID === assistantMessageID) calibration.bytes += bytes;
|
|
197
|
+
const stepID = assistantMessageID ?? "";
|
|
198
|
+
if (st.activeAssistantMessageID !== stepID) {
|
|
199
|
+
st.activeAssistantMessageID = stepID;
|
|
200
|
+
st.lastSampleAt = null;
|
|
201
|
+
st.activeTools.clear();
|
|
202
|
+
st.toolPauseStartedAt = null;
|
|
203
|
+
st.toolPausedMs = 0;
|
|
204
|
+
}
|
|
205
|
+
if (st.lastSampleAt === null) {
|
|
206
|
+
st.lastSampleAt = now;
|
|
207
|
+
if (st.activeTools.size > 0) st.toolPauseStartedAt = now;
|
|
208
|
+
} else {
|
|
209
|
+
this.advanceTiming(st, now);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Start observing one model step for a possible one-shot calibration. */
|
|
214
|
+
beginStep(sessionID, assistantMessageID) {
|
|
215
|
+
const st = this.runs.get(sessionID);
|
|
216
|
+
if (!st || st.phase !== "running") this.beginRun(sessionID);
|
|
217
|
+
const running = this.runs.get(sessionID);
|
|
218
|
+
if (running) {
|
|
219
|
+
running.activeAssistantMessageID = assistantMessageID;
|
|
220
|
+
running.lastSampleAt = null;
|
|
221
|
+
running.activeTools.clear();
|
|
222
|
+
running.toolPauseStartedAt = null;
|
|
223
|
+
running.toolPausedMs = 0;
|
|
224
|
+
}
|
|
225
|
+
if (this.ratios.has(sessionID)) return;
|
|
226
|
+
this.calibrationSteps.set(sessionID, {
|
|
227
|
+
assistantMessageID,
|
|
228
|
+
bytes: 0
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Pair a completed step's usage with deltas from that assistant message. */
|
|
233
|
+
finishStep(sessionID, assistantMessageID, generatedTokens, now) {
|
|
234
|
+
const st = this.runs.get(sessionID);
|
|
235
|
+
if (st?.activeAssistantMessageID === assistantMessageID) {
|
|
236
|
+
this.advanceTiming(st, now);
|
|
237
|
+
st.activeAssistantMessageID = null;
|
|
238
|
+
st.lastSampleAt = null;
|
|
239
|
+
st.activeTools.clear();
|
|
240
|
+
st.toolPauseStartedAt = null;
|
|
241
|
+
st.toolPausedMs = 0;
|
|
242
|
+
}
|
|
243
|
+
const calibration = this.calibrationSteps.get(sessionID);
|
|
244
|
+
if (!calibration || calibration.assistantMessageID !== assistantMessageID) return;
|
|
245
|
+
this.calibrationSteps.delete(sessionID);
|
|
246
|
+
if (this.ratios.has(sessionID)) return;
|
|
247
|
+
if (generatedTokens === undefined || !Number.isFinite(generatedTokens) || generatedTokens < CALIBRATION_MIN_TOKENS) return;
|
|
248
|
+
const bytes = calibration.bytes;
|
|
249
|
+
if (bytes < CALIBRATION_MIN_BYTES) return;
|
|
250
|
+
const ratio = bytes / generatedTokens;
|
|
251
|
+
if (ratio < BYTES_PER_TOKEN_MIN || ratio > BYTES_PER_TOKEN_MAX) {
|
|
252
|
+
mark(`calibration rejected sid=${sessionID} bytes=${bytes} tokens=${generatedTokens} ratio=${ratio.toFixed(2)}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.ratios.set(sessionID, ratio);
|
|
256
|
+
mark(`calibrated sid=${sessionID} bytes=${bytes} tokens=${generatedTokens} ratio=${ratio.toFixed(2)}`);
|
|
257
|
+
}
|
|
258
|
+
beginTool(sessionID, assistantMessageID, toolID, now) {
|
|
259
|
+
const st = this.runs.get(sessionID);
|
|
260
|
+
if (!st || st.activeAssistantMessageID !== assistantMessageID || st.activeTools.has(toolID)) return;
|
|
261
|
+
if (st.activeTools.size === 0 && st.lastSampleAt !== null) st.toolPauseStartedAt = now;
|
|
262
|
+
st.activeTools.add(toolID);
|
|
263
|
+
}
|
|
264
|
+
finishTool(sessionID, assistantMessageID, toolID, now) {
|
|
265
|
+
const st = this.runs.get(sessionID);
|
|
266
|
+
if (st?.activeAssistantMessageID !== assistantMessageID || !st.activeTools.delete(toolID) || st.activeTools.size > 0) return;
|
|
267
|
+
if (st.toolPauseStartedAt !== null) st.toolPausedMs += Math.max(0, now - st.toolPauseStartedAt);
|
|
268
|
+
st.toolPauseStartedAt = null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The session's calibrated ratio, or the configured fallback. */
|
|
272
|
+
ratioFor(sessionID) {
|
|
273
|
+
return this.ratios.get(sessionID) ?? this.config.bytesPerToken;
|
|
181
274
|
}
|
|
182
275
|
finish(sessionID, now) {
|
|
183
276
|
const st = this.runs.get(sessionID);
|
|
184
277
|
if (!st || st.phase === "ended") return;
|
|
278
|
+
this.calibrationSteps.delete(sessionID);
|
|
185
279
|
st.phase = "ended";
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
280
|
+
this.advanceTiming(st, now);
|
|
281
|
+
st.lastSampleAt = null;
|
|
282
|
+
st.activeAssistantMessageID = null;
|
|
283
|
+
st.activeTools.clear();
|
|
284
|
+
st.toolPauseStartedAt = null;
|
|
285
|
+
st.toolPausedMs = 0;
|
|
286
|
+
const tokens = estimateTokens(st.bytes, this.ratioFor(sessionID));
|
|
191
287
|
if (tokens <= 0) {
|
|
192
|
-
this.
|
|
288
|
+
this.evictStale();
|
|
193
289
|
return;
|
|
194
290
|
}
|
|
195
|
-
const seconds = Math.max(st.activeMs,
|
|
291
|
+
const seconds = Math.max(st.activeMs, 1) / 1000;
|
|
196
292
|
st.frozen = {
|
|
197
293
|
tps: tokens / seconds,
|
|
198
294
|
tokens
|
|
199
295
|
};
|
|
200
|
-
st.samples = [];
|
|
201
296
|
mark(`finish sid=${sessionID} tokens=${tokens} activeMs=${st.activeMs} tps=${st.frozen.tps.toFixed(1)}`);
|
|
297
|
+
this.evictStale();
|
|
202
298
|
}
|
|
203
|
-
|
|
299
|
+
dropSession(sessionID) {
|
|
204
300
|
this.runs.delete(sessionID);
|
|
301
|
+
this.ratios.delete(sessionID);
|
|
302
|
+
this.calibrationSteps.delete(sessionID);
|
|
205
303
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return false;
|
|
304
|
+
evict(sessionID) {
|
|
305
|
+
this.dropSession(sessionID);
|
|
209
306
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
live(st, now) {
|
|
214
|
-
const cutoff = now - this.config.sampleWindowMs;
|
|
215
|
-
const active = st.samples.filter(s => s.timestamp >= cutoff);
|
|
216
|
-
const last = active.at(-1);
|
|
217
|
-
const first = active[0];
|
|
218
|
-
if (!last || !first || now - last.timestamp > this.config.liveStaleMs) return -1;
|
|
219
|
-
let bytes = 0;
|
|
220
|
-
for (const s of active) bytes += s.bytes;
|
|
221
|
-
const tokens = bytes / BYTES_PER_TOKEN;
|
|
222
|
-
let durationMs;
|
|
223
|
-
if (active.length < 2) {
|
|
224
|
-
durationMs = Math.max(this.config.singleSampleMinMs, Math.min(now - first.timestamp, this.config.singleSampleMaxMs));
|
|
225
|
-
} else {
|
|
226
|
-
// Gaps are capped exactly as in push(): a pause for tool execution must
|
|
227
|
-
// not be charged to the model's throughput.
|
|
228
|
-
let gaps = 0;
|
|
229
|
-
let prev = first.timestamp;
|
|
230
|
-
for (const s of active) {
|
|
231
|
-
gaps += Math.min(Math.max(0, s.timestamp - prev), this.config.gapCapMs);
|
|
232
|
-
prev = s.timestamp;
|
|
233
|
-
}
|
|
234
|
-
gaps += Math.min(now - last.timestamp, this.config.tailMaxMs);
|
|
235
|
-
durationMs = Math.max(gaps, this.config.singleSampleMinMs);
|
|
307
|
+
hasRunning() {
|
|
308
|
+
for (const st of this.runs.values()) {
|
|
309
|
+
if (st.phase === "running" && st.activeAssistantMessageID !== null && st.lastSampleAt !== null) return true;
|
|
236
310
|
}
|
|
237
|
-
return
|
|
311
|
+
return false;
|
|
238
312
|
}
|
|
239
313
|
value(sessionID, now) {
|
|
240
314
|
const st = this.runs.get(sessionID);
|
|
@@ -245,34 +319,17 @@ export class TpsTracker {
|
|
|
245
319
|
frozen: true
|
|
246
320
|
};
|
|
247
321
|
if (st.phase !== "running") return null;
|
|
248
|
-
const tokens = estimateTokens(st.bytes);
|
|
322
|
+
const tokens = estimateTokens(st.bytes, this.ratioFor(sessionID));
|
|
249
323
|
if (tokens <= 0) return null;
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
tokens,
|
|
254
|
-
frozen: false
|
|
255
|
-
};
|
|
256
|
-
// Live window empty (long tool call, sub-agent turn): keep showing the
|
|
257
|
-
// run-so-far average instead of blanking. It uses the same elapsed-time
|
|
258
|
-
// formula finish() will use, so freezing does not make the number jump.
|
|
259
|
-
const tail = st.lastSampleAt === null ? 0 : Math.min(Math.max(0, now - st.lastSampleAt), this.config.tailMaxMs);
|
|
260
|
-
const seconds = Math.max(st.activeMs + tail, this.config.singleSampleMinMs) / 1000;
|
|
324
|
+
const currentPause = st.toolPauseStartedAt === null ? 0 : Math.max(0, now - st.toolPauseStartedAt);
|
|
325
|
+
const activeTail = st.lastSampleAt === null ? 0 : Math.max(0, now - st.lastSampleAt - st.toolPausedMs - currentPause);
|
|
326
|
+
const seconds = Math.max(st.activeMs + activeTail, 1) / 1000;
|
|
261
327
|
return {
|
|
262
328
|
tps: tokens / seconds,
|
|
263
329
|
tokens,
|
|
264
330
|
frozen: false
|
|
265
331
|
};
|
|
266
332
|
}
|
|
267
|
-
prune(now) {
|
|
268
|
-
const cutoff = now - this.config.sampleWindowMs;
|
|
269
|
-
for (const st of this.runs.values()) {
|
|
270
|
-
// Finished runs keep their frozen average until the next run replaces
|
|
271
|
-
// them, so only the live sample window needs trimming.
|
|
272
|
-
const oldest = st.samples[0];
|
|
273
|
-
if (oldest && oldest.timestamp < cutoff) st.samples = st.samples.filter(s => s.timestamp >= cutoff);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
333
|
}
|
|
277
334
|
|
|
278
335
|
// ---------------------------------------------------------------------------
|
|
@@ -308,17 +365,10 @@ function clampNumber(value, fallback, min, max) {
|
|
|
308
365
|
return Math.min(Math.max(value, min), max);
|
|
309
366
|
}
|
|
310
367
|
export function resolveOptions(raw) {
|
|
311
|
-
const singleSampleMinMs = clampNumber(raw.singleSampleMinMs, DEFAULT_OPTIONS.singleSampleMinMs, 50, 5_000);
|
|
312
368
|
return {
|
|
313
369
|
display: isDisplayMode(raw.display) ? raw.display : DEFAULT_OPTIONS.display,
|
|
314
370
|
refreshHz: clampNumber(raw.refreshHz, DEFAULT_OPTIONS.refreshHz, 1, 60),
|
|
315
|
-
|
|
316
|
-
liveStaleMs: clampNumber(raw.liveStaleMs, DEFAULT_OPTIONS.liveStaleMs, 250, 30_000),
|
|
317
|
-
singleSampleMinMs,
|
|
318
|
-
// The ceiling can never sit below the floor, whatever the user wrote.
|
|
319
|
-
singleSampleMaxMs: Math.max(singleSampleMinMs, clampNumber(raw.singleSampleMaxMs, DEFAULT_OPTIONS.singleSampleMaxMs, 50, 10_000)),
|
|
320
|
-
tailMaxMs: clampNumber(raw.tailMaxMs, DEFAULT_OPTIONS.tailMaxMs, 0, 10_000),
|
|
321
|
-
gapCapMs: clampNumber(raw.gapCapMs, DEFAULT_OPTIONS.gapCapMs, 100, 30_000),
|
|
371
|
+
bytesPerToken: clampNumber(raw.bytesPerToken, DEFAULT_OPTIONS.bytesPerToken, BYTES_PER_TOKEN_MIN, BYTES_PER_TOKEN_MAX),
|
|
322
372
|
debug: raw.debug === true
|
|
323
373
|
};
|
|
324
374
|
}
|
|
@@ -370,11 +420,9 @@ const definition = {
|
|
|
370
420
|
stopTimer();
|
|
371
421
|
return;
|
|
372
422
|
}
|
|
373
|
-
const now = Date.now();
|
|
374
|
-
tracker.prune(now);
|
|
375
423
|
const running = tracker.hasRunning();
|
|
376
|
-
// While
|
|
377
|
-
//
|
|
424
|
+
// While a model step is running, elapsed generation time changes even
|
|
425
|
+
// without new deltas, so republish every tick to expose provider stalls.
|
|
378
426
|
if (dirty || running) {
|
|
379
427
|
dirty = false;
|
|
380
428
|
setVersion(v => v + 1);
|
|
@@ -394,7 +442,7 @@ const definition = {
|
|
|
394
442
|
};
|
|
395
443
|
const onDelta = e => {
|
|
396
444
|
if (!isActive()) return;
|
|
397
|
-
tracker.push(e.data.sessionID, e.data.delta, Date.now());
|
|
445
|
+
tracker.push(e.data.sessionID, e.data.delta, Date.now(), e.data.assistantMessageID);
|
|
398
446
|
touch();
|
|
399
447
|
};
|
|
400
448
|
const onFinish = e => {
|
|
@@ -402,6 +450,24 @@ const definition = {
|
|
|
402
450
|
tracker.finish(e.data.sessionID, Date.now());
|
|
403
451
|
touch();
|
|
404
452
|
};
|
|
453
|
+
const onStepStarted = e => {
|
|
454
|
+
if (!isActive()) return;
|
|
455
|
+
tracker.beginStep(e.data.sessionID, e.data.assistantMessageID);
|
|
456
|
+
};
|
|
457
|
+
const onStepFinished = e => {
|
|
458
|
+
if (!isActive()) return;
|
|
459
|
+
const tokens = e.data.tokens;
|
|
460
|
+
tracker.finishStep(e.data.sessionID, e.data.assistantMessageID, tokens === undefined ? undefined : tokens.output + tokens.reasoning, Date.now());
|
|
461
|
+
touch();
|
|
462
|
+
};
|
|
463
|
+
const onToolStarted = e => {
|
|
464
|
+
if (!isActive()) return;
|
|
465
|
+
tracker.beginTool(e.data.sessionID, e.data.assistantMessageID, e.data.id, Date.now());
|
|
466
|
+
};
|
|
467
|
+
const onToolFinished = e => {
|
|
468
|
+
if (!isActive()) return;
|
|
469
|
+
tracker.finishTool(e.data.sessionID, e.data.assistantMessageID, e.data.id, Date.now());
|
|
470
|
+
};
|
|
405
471
|
const unsubs = [ctx.data.on("session.execution.started", e => {
|
|
406
472
|
if (!isActive()) return;
|
|
407
473
|
tracker.beginRun(e.data.sessionID);
|
|
@@ -409,7 +475,7 @@ const definition = {
|
|
|
409
475
|
}), ctx.data.on("session.text.delta", onDelta), ctx.data.on("session.reasoning.delta", onDelta),
|
|
410
476
|
// Tool arguments are model output too: without this the indicator blanks
|
|
411
477
|
// partway through every large write/edit while generation is at full rate.
|
|
412
|
-
ctx.data.on("session.tool.input.delta", onDelta), ctx.data.on("session.execution.succeeded", onFinish), ctx.data.on("session.execution.failed", onFinish), ctx.data.on("session.execution.interrupted", onFinish), ctx.data.on("session.idle", onFinish), ctx.data.on("session.deleted", e => {
|
|
478
|
+
ctx.data.on("session.tool.input.delta", onDelta), ctx.data.on("session.step.started", onStepStarted), ctx.data.on("session.step.ended", onStepFinished), ctx.data.on("session.step.failed", onStepFinished), ctx.data.on("session.tool.called", onToolStarted), ctx.data.on("session.tool.success", onToolFinished), ctx.data.on("session.tool.failed", onToolFinished), ctx.data.on("session.execution.succeeded", onFinish), ctx.data.on("session.execution.failed", onFinish), ctx.data.on("session.execution.interrupted", onFinish), ctx.data.on("session.idle", onFinish), ctx.data.on("session.deleted", e => {
|
|
413
479
|
if (!isActive()) return;
|
|
414
480
|
tracker.evict(e.data.sessionID);
|
|
415
481
|
touch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode2-tps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-rc.1",
|
|
4
4
|
"description": "Live token-throughput indicator for the OpenCode 2 TUI prompt composer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "P-Theo",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@opentui/solid": ">=0.5.
|
|
31
|
+
"@opentui/solid": ">=0.5.4",
|
|
32
32
|
"solid-js": "^1.9.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependenciesMeta": {
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@babel/core": "^7.29.7",
|
|
49
49
|
"@babel/preset-typescript": "^7.29.7",
|
|
50
|
-
"@opencode-ai/cli": "0.0.0-beta-
|
|
51
|
-
"@opencode-ai/plugin": "0.0.0-beta-
|
|
52
|
-
"@opencode-ai/theme": "0.0.0-beta-
|
|
53
|
-
"@opentui/core": "^0.5.
|
|
54
|
-
"@opentui/solid": "^0.5.
|
|
50
|
+
"@opencode-ai/cli": "0.0.0-beta-17639",
|
|
51
|
+
"@opencode-ai/plugin": "0.0.0-beta-17639",
|
|
52
|
+
"@opencode-ai/theme": "0.0.0-beta-17639",
|
|
53
|
+
"@opentui/core": "^0.5.4",
|
|
54
|
+
"@opentui/solid": "^0.5.4",
|
|
55
55
|
"@oxlint/plugins": "^1.78.0",
|
|
56
56
|
"@types/bun": "^1.3.14",
|
|
57
57
|
"@types/node": "^24.0.0",
|