opencode-plugin-context 1.1.1 → 1.2.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 +21 -1
- package/dist/tui.js +134 -9
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -14,9 +14,10 @@ Context
|
|
|
14
14
|
━━━━━━━━━━━━━━━━━ 69%
|
|
15
15
|
138K / 200K tokens
|
|
16
16
|
$0.04 spent
|
|
17
|
-
|
|
17
|
+
62.4 TPS · avg 48.1 · 23s
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
One color-coded legend row follows the bar — `▍` marker in the segment's color,
|
|
21
22
|
then a muted letter + count. Colors follow the active theme:
|
|
22
23
|
|
|
@@ -66,6 +67,25 @@ always reaches full width. A very small segment may not fill a single bar cell
|
|
|
66
67
|
always visible in the legend. Percent is colored like the usage plugin: green
|
|
67
68
|
`<50%`, amber `50–74%`, orange `75–99%`, red `100%`.
|
|
68
69
|
|
|
70
|
+
## Throughput (TPS)
|
|
71
|
+
|
|
72
|
+
While the assistant streams, a live tokens-per-second line is appended below the
|
|
73
|
+
totals:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
62.4 TPS · avg 48.1 · 23s
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **instant** — smoothed rate over a 1s rolling window, colored by speed: red
|
|
80
|
+
`<10`, amber `10–49`, green `≥50`. Shown only while tokens are arriving.
|
|
81
|
+
- **avg** — tokens / active generation span (first → last token), so idle time
|
|
82
|
+
doesn't drag it down. Shown once the stream goes quiet.
|
|
83
|
+
- **elapsed** — active generation span.
|
|
84
|
+
|
|
85
|
+
Counts come from `session.text.delta` / `session.reasoning.delta`, estimated with
|
|
86
|
+
the same chars/4 heuristic used elsewhere (sub-token deltas are carried over), so
|
|
87
|
+
it tracks the live stream rather than the provider's final totals.
|
|
88
|
+
|
|
69
89
|
## Configuration
|
|
70
90
|
|
|
71
91
|
All options are optional. Plugin entry in `tui.json`:
|
package/dist/tui.js
CHANGED
|
@@ -56,6 +56,79 @@ function computeContext(counts, limits, cost = 0, estimates, exclude = []) {
|
|
|
56
56
|
known: window > 0
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
+
var MIN_WINDOW_SECONDS = 0.3;
|
|
60
|
+
var MAX_INITIAL_TPS = 100;
|
|
61
|
+
function createTpsTracker(options = {}) {
|
|
62
|
+
const windowMs = options.windowMs && options.windowMs > 0 ? options.windowMs : 1e3;
|
|
63
|
+
const halfLifeMs = options.halfLifeMs && options.halfLifeMs > 0 ? options.halfLifeMs : 250;
|
|
64
|
+
let samples = [];
|
|
65
|
+
let total = 0;
|
|
66
|
+
let start = -1;
|
|
67
|
+
let last = -1;
|
|
68
|
+
let smoothed = 0;
|
|
69
|
+
let smoothedAt = 0;
|
|
70
|
+
let hasSmoothed = false;
|
|
71
|
+
function raw(now) {
|
|
72
|
+
if (samples.length === 0) return 0;
|
|
73
|
+
const cutoff = now - windowMs;
|
|
74
|
+
let tokens = 0;
|
|
75
|
+
let oldest = now;
|
|
76
|
+
for (const sample of samples) {
|
|
77
|
+
if (sample.t >= cutoff) {
|
|
78
|
+
tokens += sample.count;
|
|
79
|
+
if (sample.t < oldest) oldest = sample.t;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (tokens === 0) return 0;
|
|
83
|
+
return tokens / Math.max((now - oldest) / 1e3, MIN_WINDOW_SECONDS);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
record(count, timestamp) {
|
|
87
|
+
if (!(count > 0)) return;
|
|
88
|
+
const t = timestamp ?? Date.now();
|
|
89
|
+
if (start < 0) start = t;
|
|
90
|
+
if (t > last) last = t;
|
|
91
|
+
total += count;
|
|
92
|
+
samples.push({ t, count });
|
|
93
|
+
const cutoff = t - windowMs;
|
|
94
|
+
let drop = 0;
|
|
95
|
+
while (drop < samples.length && samples[drop].t < cutoff) drop++;
|
|
96
|
+
if (drop > 0) samples = samples.slice(drop);
|
|
97
|
+
const value = raw(t);
|
|
98
|
+
if (!hasSmoothed) {
|
|
99
|
+
smoothed = Math.min(value, MAX_INITIAL_TPS);
|
|
100
|
+
hasSmoothed = true;
|
|
101
|
+
} else {
|
|
102
|
+
const dt = Math.max(1, t - smoothedAt);
|
|
103
|
+
const alpha = Math.exp(-Math.LN2 * dt / halfLifeMs);
|
|
104
|
+
smoothed = alpha * smoothed + (1 - alpha) * value;
|
|
105
|
+
}
|
|
106
|
+
smoothedAt = t;
|
|
107
|
+
},
|
|
108
|
+
instant() {
|
|
109
|
+
if (!hasSmoothed || Date.now() - last > windowMs) return 0;
|
|
110
|
+
return smoothed;
|
|
111
|
+
},
|
|
112
|
+
average() {
|
|
113
|
+
return start < 0 ? 0 : total / Math.max((last - start) / 1e3, MIN_WINDOW_SECONDS);
|
|
114
|
+
},
|
|
115
|
+
total() {
|
|
116
|
+
return total;
|
|
117
|
+
},
|
|
118
|
+
elapsed() {
|
|
119
|
+
return start < 0 ? 0 : last - start;
|
|
120
|
+
},
|
|
121
|
+
reset() {
|
|
122
|
+
samples = [];
|
|
123
|
+
total = 0;
|
|
124
|
+
start = -1;
|
|
125
|
+
last = -1;
|
|
126
|
+
smoothed = 0;
|
|
127
|
+
smoothedAt = 0;
|
|
128
|
+
hasSmoothed = false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
59
132
|
function segmentBar(segments, window, width, exclude = []) {
|
|
60
133
|
if (window <= 0 || width <= 0) return [];
|
|
61
134
|
let remaining = width;
|
|
@@ -117,27 +190,57 @@ var plugin = {
|
|
|
117
190
|
setRenderTick((n) => n + 1);
|
|
118
191
|
api.renderer.requestRender();
|
|
119
192
|
};
|
|
193
|
+
const trackers = /* @__PURE__ */ new Map();
|
|
194
|
+
const carries = /* @__PURE__ */ new Map();
|
|
195
|
+
const trackerFor = (sessionId) => {
|
|
196
|
+
let tracker = trackers.get(sessionId);
|
|
197
|
+
if (!tracker) {
|
|
198
|
+
tracker = createTpsTracker();
|
|
199
|
+
trackers.set(sessionId, tracker);
|
|
200
|
+
}
|
|
201
|
+
return tracker;
|
|
202
|
+
};
|
|
203
|
+
const onDelta = (sessionId, text, at) => {
|
|
204
|
+
const carry = (carries.get(sessionId) ?? 0) + text.length;
|
|
205
|
+
const tokens = Math.floor(carry / 4);
|
|
206
|
+
carries.set(sessionId, carry - tokens * 4);
|
|
207
|
+
if (tokens > 0) trackerFor(sessionId).record(tokens, at);
|
|
208
|
+
throttledRepaint();
|
|
209
|
+
};
|
|
210
|
+
let lastRepaint = 0;
|
|
211
|
+
let pendingRepaint;
|
|
212
|
+
const throttledRepaint = () => {
|
|
213
|
+
const wait = lastRepaint + 50 - Date.now();
|
|
214
|
+
if (wait <= 0) {
|
|
215
|
+
lastRepaint = Date.now();
|
|
216
|
+
repaint();
|
|
217
|
+
} else if (pendingRepaint === void 0) {
|
|
218
|
+
pendingRepaint = setTimeout(() => {
|
|
219
|
+
pendingRepaint = void 0;
|
|
220
|
+
lastRepaint = Date.now();
|
|
221
|
+
repaint();
|
|
222
|
+
}, wait);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
120
225
|
const unsubs = [
|
|
121
|
-
api.event.on("
|
|
122
|
-
api.event.on("message.part.updated", repaint),
|
|
123
|
-
api.event.on("message.part.removed", repaint),
|
|
124
|
-
api.event.on("message.removed", repaint),
|
|
125
|
-
api.event.on("session.updated", repaint),
|
|
126
|
-
api.event.on("session.compacted", repaint),
|
|
226
|
+
api.event.on("session.created", repaint),
|
|
127
227
|
api.event.on("session.status", repaint),
|
|
128
|
-
api.event.on("session.idle", repaint)
|
|
228
|
+
api.event.on("session.idle", repaint),
|
|
229
|
+
api.event.on("session.text.delta", (event) => onDelta(event.data.sessionID, event.data.delta, event.created)),
|
|
230
|
+
api.event.on("session.reasoning.delta", (event) => onDelta(event.data.sessionID, event.data.delta, event.created))
|
|
129
231
|
];
|
|
130
232
|
const repaintTimer = setInterval(repaint, 2e3);
|
|
131
233
|
api.lifecycle.onDispose(() => {
|
|
132
234
|
for (const unsub of unsubs) unsub();
|
|
133
235
|
clearInterval(repaintTimer);
|
|
236
|
+
if (pendingRepaint !== void 0) clearTimeout(pendingRepaint);
|
|
134
237
|
});
|
|
135
238
|
api.slots.register({
|
|
136
239
|
order: SLOT_ORDER,
|
|
137
240
|
slots: {
|
|
138
241
|
sidebar_content(_ctx, props) {
|
|
139
242
|
getRenderTick();
|
|
140
|
-
return renderPanel(api, props.session_id, config);
|
|
243
|
+
return renderPanel(api, props.session_id, config, trackers.get(props.session_id));
|
|
141
244
|
}
|
|
142
245
|
}
|
|
143
246
|
});
|
|
@@ -196,7 +299,7 @@ function sessionUsage(api, sessionId, config) {
|
|
|
196
299
|
const estimates = config.estimate ? collectEstimates(api, sessionId) : void 0;
|
|
197
300
|
return computeContext(counts, limits, cost, estimates, config.exclude);
|
|
198
301
|
}
|
|
199
|
-
function renderPanel(api, sessionId, config) {
|
|
302
|
+
function renderPanel(api, sessionId, config, tps) {
|
|
200
303
|
const theme = api.theme.current;
|
|
201
304
|
const usage = sessionUsage(api, sessionId, config);
|
|
202
305
|
const header = /* @__PURE__ */ jsx("text", { fg: theme.text, attributes: BOLD, children: "Context" });
|
|
@@ -264,8 +367,30 @@ function renderPanel(api, sessionId, config) {
|
|
|
264
367
|
if (usage.cost > 0) {
|
|
265
368
|
lines.push(/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: `${money.format(usage.cost)} spent` }));
|
|
266
369
|
}
|
|
370
|
+
if (tps && tps.total() > 0) {
|
|
371
|
+
const instant = tps.instant();
|
|
372
|
+
const live = instant > 0.05;
|
|
373
|
+
const avg = tps.average();
|
|
374
|
+
lines.push(
|
|
375
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
|
|
376
|
+
live ? /* @__PURE__ */ jsx("text", { fg: speedColor(instant, theme), children: `${instant.toFixed(1)} TPS` }) : null,
|
|
377
|
+
/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: live ? ` \xB7 avg ${avg.toFixed(1)} \xB7 ${formatDuration(tps.elapsed())}` : `avg ${avg.toFixed(1)} TPS \xB7 ${formatDuration(tps.elapsed())}` })
|
|
378
|
+
] })
|
|
379
|
+
);
|
|
380
|
+
}
|
|
267
381
|
return /* @__PURE__ */ jsx("box", { width: "100%", flexDirection: "column", children: lines });
|
|
268
382
|
}
|
|
383
|
+
function formatDuration(ms) {
|
|
384
|
+
const seconds = ms / 1e3;
|
|
385
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
386
|
+
const whole = Math.round(seconds);
|
|
387
|
+
return `${Math.floor(whole / 60)}m${String(whole % 60).padStart(2, "0")}s`;
|
|
388
|
+
}
|
|
389
|
+
function speedColor(tps, theme) {
|
|
390
|
+
if (tps < 10) return theme.error;
|
|
391
|
+
if (tps < 50) return theme.warning;
|
|
392
|
+
return theme.success;
|
|
393
|
+
}
|
|
269
394
|
function segmentColor(id, theme, estimate) {
|
|
270
395
|
const base = {
|
|
271
396
|
cached: theme.success,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-plugin-context",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "OpenCode TUI plugin that renders the session's context-window usage as a colored, segmented bar (cached / prompt / thinking / output / reserved output) in the sidebar",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@opencode-ai/plugin": ">=
|
|
43
|
+
"@opencode-ai/plugin": ">=0.0.0-next-17444",
|
|
44
44
|
"@opentui/core": ">=0.2.9",
|
|
45
45
|
"@opentui/solid": ">=0.2.9"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@opencode-ai/plugin": "
|
|
49
|
-
"@opentui/core": "0.5.
|
|
50
|
-
"@opentui/solid": "0.5.
|
|
48
|
+
"@opencode-ai/plugin": "0.0.0-next-17444",
|
|
49
|
+
"@opentui/core": "0.5.11",
|
|
50
|
+
"@opentui/solid": "0.5.11",
|
|
51
51
|
"@types/node": "^26.2.0",
|
|
52
52
|
"esbuild": "^0.28.0",
|
|
53
53
|
"solid-js": "1.9.12",
|