opencode2-tps 0.2.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +14 -6
  2. package/dist/tui.js +24 -2
  3. package/package.json +6 -7
package/README.md CHANGED
@@ -12,9 +12,15 @@ When OpenCode reports terminal usage, the token count becomes exact while TPS re
12
12
 
13
13
  ## Install
14
14
 
15
- 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. If the figure never appears, check your version first.
15
+ 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-19381`. The TUI plugin API is still moving, so a much newer or older build may drop the indicator without an error. If the figure never appears, check your version first.
16
16
 
17
- Add the package to `~/.config/opencode/cli.json`:
17
+ Install it with the CLI's plugin command, which adds the entry to `~/.config/opencode/cli.json` for you:
18
+
19
+ ```sh
20
+ opencode2 plugin add opencode2-tps
21
+ ```
22
+
23
+ Or add the package to `~/.config/opencode/cli.json` yourself:
18
24
 
19
25
  ```json
20
26
  {
@@ -35,12 +41,12 @@ To set options, use the object form:
35
41
  }
36
42
  ```
37
43
 
38
- A running TUI picks up `cli.json` changes immediately. On first use it installs the package into its own cache, `~/.cache/opencode/packages/opencode2-tps/` on Linux.
44
+ A running TUI picks up `cli.json` changes immediately. On first use it installs the package into its own cache under `~/.cache/opencode/npm/` on Linux — one millisecond-timestamped generation per spec, for example `opencode2-tps@latest/<generation>/`, with the newest generation winning.
39
45
 
40
- That cache is keyed on the entry text and is never refreshed once it exists, so a restart alone will not pick up a new release. To upgrade, delete the directory and restart:
46
+ The host reuses the newest installed generation without contacting the registry, so a restart alone may not pick up a new release. To upgrade, delete the spec's cache directory and restart:
41
47
 
42
48
  ```sh
43
- rm -rf ~/.cache/opencode/packages/opencode2-tps
49
+ rm -rf ~/.cache/opencode/npm/opencode2-tps@latest
44
50
  ```
45
51
 
46
52
  To pin a version instead, put the range in the entry — `"opencode2-tps@0.1.0"`. Every distinct entry gets its own cache directory.
@@ -67,7 +73,9 @@ The defaults are usable as they are. For the full option list, the ranges and mo
67
73
 
68
74
  While output streams, the plugin estimates tokens from observable UTF-8 bytes at a default of 4.75 bytes per token and calculates a bounded rolling delivery rate. Complete text, reasoning, and tool-input events reconcile buffered or missed deltas without creating artificial live-rate spikes.
69
75
 
70
- At the end of each model step, OpenCode's reported output and reasoning usage replaces the byte estimate. Settled TPS divides those exact tokens by observed step spans ending at the final model-content boundary, which excludes later local tool execution and time between model calls.
76
+ At the end of each model step, OpenCode's reported output and reasoning usage replaces the byte estimate. Settled TPS divides those exact tokens by observed step spans ending at `session.step.streamed`, the host's authoritative end of the model stream, published before local tools join. Hosts that do not publish the event fall back to the final model-content boundary. Either way, local tool execution and time between model calls are excluded.
77
+
78
+ OpenCode's built-in assistant-footer t/s divides visible output tokens by the same step spans, leaving hidden reasoning out of its numerator. This plugin counts output plus reasoning, so on reasoning models its settled figure reads higher than the built-in one — those tokens were generated too.
71
79
 
72
80
  TPS is always approximate (`~`) because OpenCode does not expose token-level timestamps. Proprietary reasoning may be encrypted or represented only by a short summary, and some providers buffer tool arguments until completion. During those opaque intervals the live rate holds or becomes unavailable instead of continuously falling. Opaque provider state is never counted by byte length.
73
81
 
package/dist/tui.js CHANGED
@@ -176,6 +176,7 @@ export class TpsTracker {
176
176
  const step = {
177
177
  assistantMessageID,
178
178
  startedAt: now,
179
+ streamedAt: null,
179
180
  lastBoundaryAt: null,
180
181
  observableBytes: 0,
181
182
  blocks: new Map(),
@@ -242,6 +243,19 @@ export class TpsTracker {
242
243
  step.observableBytes += block.finalBytes - block.streamedBytes;
243
244
  step.lastBoundaryAt = Math.max(step.lastBoundaryAt ?? now, now);
244
245
  }
246
+
247
+ /**
248
+ * The host's authoritative end of the model stream, published after the
249
+ * provider stream exits and before local tools join. Assigned rather than
250
+ * maxed so a retried attempt reusing the message ID moves the boundary to its
251
+ * own completion.
252
+ */
253
+ markStreamed(sessionID, assistantMessageID, now) {
254
+ const st = this.runs.get(sessionID);
255
+ const step = st?.activeStep;
256
+ if (!step || step.assistantMessageID !== assistantMessageID) return;
257
+ step.streamedAt = now;
258
+ }
245
259
  settleActiveStep(st, generatedTokens) {
246
260
  const step = st.activeStep;
247
261
  if (!step) return;
@@ -251,7 +265,10 @@ export class TpsTracker {
251
265
  st.tokensEstimated = true;
252
266
  st.partial = true;
253
267
  }
254
- if (step.lastBoundaryAt !== null) st.settledDurationMs += Math.max(0, step.lastBoundaryAt - step.startedAt);
268
+ // `session.step.streamed` is the exact stream end; the last content boundary
269
+ // remains the fallback for hosts that do not publish it.
270
+ const end = step.streamedAt ?? step.lastBoundaryAt;
271
+ if (end !== null) st.settledDurationMs += Math.max(0, end - step.startedAt);
255
272
  st.settledSteps.add(step.assistantMessageID);
256
273
  st.activeStep = null;
257
274
  }
@@ -478,6 +495,11 @@ const definition = {
478
495
  tracker.beginStep(e.data.sessionID, e.data.assistantMessageID, e.created);
479
496
  touch();
480
497
  };
498
+ const onStepStreamed = e => {
499
+ if (!isActive() || !isNewEvent(e)) return;
500
+ tracker.markStreamed(e.data.sessionID, e.data.assistantMessageID, e.created);
501
+ touch();
502
+ };
481
503
  const onStepFinished = e => {
482
504
  if (!isActive() || !isNewEvent(e)) return;
483
505
  const tokens = e.data.tokens;
@@ -489,7 +511,7 @@ const definition = {
489
511
  if (!isActive() || !isNewEvent(e)) return;
490
512
  tracker.beginRun(e.data.sessionID);
491
513
  touch();
492
- }), ctx.data.on("session.text.delta", onDelta), ctx.data.on("session.reasoning.delta", onDelta), ctx.data.on("session.tool.input.delta", onDelta), ctx.data.on("session.text.started", onBlockStarted), ctx.data.on("session.reasoning.started", onBlockStarted), ctx.data.on("session.tool.input.started", onBlockStarted), ctx.data.on("session.text.ended", onBlockEnded), ctx.data.on("session.reasoning.ended", onBlockEnded), ctx.data.on("session.tool.input.ended", onBlockEnded), 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.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 => {
514
+ }), ctx.data.on("session.text.delta", onDelta), ctx.data.on("session.reasoning.delta", onDelta), ctx.data.on("session.tool.input.delta", onDelta), ctx.data.on("session.text.started", onBlockStarted), ctx.data.on("session.reasoning.started", onBlockStarted), ctx.data.on("session.tool.input.started", onBlockStarted), ctx.data.on("session.text.ended", onBlockEnded), ctx.data.on("session.reasoning.ended", onBlockEnded), ctx.data.on("session.tool.input.ended", onBlockEnded), ctx.data.on("session.step.started", onStepStarted), ctx.data.on("session.step.streamed", onStepStreamed), ctx.data.on("session.step.ended", onStepFinished), ctx.data.on("session.step.failed", onStepFinished), 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 => {
493
515
  if (!isActive() || !isNewEvent(e)) return;
494
516
  tracker.evict(e.data.sessionID);
495
517
  touch();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode2-tps",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Live token-throughput indicator for the OpenCode 2 TUI prompt composer",
5
5
  "license": "MIT",
6
6
  "author": "P-Theo",
@@ -21,7 +21,6 @@
21
21
  "throughput"
22
22
  ],
23
23
  "exports": {
24
- ".": "./dist/tui.js",
25
24
  "./tui": "./dist/tui.js"
26
25
  },
27
26
  "files": [
@@ -47,11 +46,11 @@
47
46
  "devDependencies": {
48
47
  "@babel/core": "^7.29.7",
49
48
  "@babel/preset-typescript": "^7.29.7",
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",
49
+ "@opencode/cli": "0.0.0-beta-19381",
50
+ "@opencode/plugin": "0.0.0-beta-19381",
51
+ "@opencode/theme": "0.0.0-beta-19381",
52
+ "@opentui/core": "^0.5.10",
53
+ "@opentui/solid": "^0.5.10",
55
54
  "@oxlint/plugins": "^1.78.0",
56
55
  "@types/bun": "^1.3.14",
57
56
  "@types/node": "^24.0.0",