opencode-plugin-context 1.1.0 → 1.1.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/dist/tui.js +166 -54
- package/package.json +4 -4
package/dist/tui.js
CHANGED
|
@@ -21,7 +21,27 @@ function num(v) {
|
|
|
21
21
|
return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.round(v) : 0;
|
|
22
22
|
}
|
|
23
23
|
function estimateTokens(input) {
|
|
24
|
-
return Math.max(0, Math.
|
|
24
|
+
return Math.max(0, Math.ceil(input.length / 4));
|
|
25
|
+
}
|
|
26
|
+
function formatCompactTokens(tokens) {
|
|
27
|
+
if (tokens <= 0) return "0";
|
|
28
|
+
const k = Math.round(tokens / 1e3);
|
|
29
|
+
return `${Math.max(1, k)}k`;
|
|
30
|
+
}
|
|
31
|
+
function scaleToPrompt(raw, prompt) {
|
|
32
|
+
const estimated = raw.system + raw.user + raw.assistant + raw.tool;
|
|
33
|
+
if (estimated <= prompt) {
|
|
34
|
+
return { ...raw, other: prompt - estimated };
|
|
35
|
+
}
|
|
36
|
+
const scale = prompt / estimated;
|
|
37
|
+
const scaled = {
|
|
38
|
+
system: Math.floor(raw.system * scale),
|
|
39
|
+
user: Math.floor(raw.user * scale),
|
|
40
|
+
assistant: Math.floor(raw.assistant * scale),
|
|
41
|
+
tool: Math.floor(raw.tool * scale)
|
|
42
|
+
};
|
|
43
|
+
const total = scaled.system + scaled.user + scaled.assistant + scaled.tool;
|
|
44
|
+
return { ...scaled, other: Math.max(0, prompt - total) };
|
|
25
45
|
}
|
|
26
46
|
function computeContext(counts, limits, cost = 0, estimates, exclude = []) {
|
|
27
47
|
const { input, output, reasoning, cacheRead, cacheWrite } = counts;
|
|
@@ -30,28 +50,62 @@ function computeContext(counts, limits, cost = 0, estimates, exclude = []) {
|
|
|
30
50
|
const reserved = limits && limits.output > 0 ? Math.max(0, limits.output - output) : 0;
|
|
31
51
|
const free = window > 0 ? Math.max(0, window - used - reserved) : 0;
|
|
32
52
|
const prompt = input + cacheWrite;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
const normalizeExclude = (id) => {
|
|
54
|
+
if (id === "tools" || id === "tool") return exclude.includes("tools") || exclude.includes("tool");
|
|
55
|
+
return exclude.includes(id);
|
|
56
|
+
};
|
|
57
|
+
const excluded = (id) => normalizeExclude(id);
|
|
58
|
+
let raw;
|
|
59
|
+
if (estimates) {
|
|
60
|
+
const hasWebCats = estimates.system !== void 0 || estimates.assistant !== void 0 || estimates.tool !== void 0;
|
|
61
|
+
if (hasWebCats) {
|
|
62
|
+
const rawTokens = {
|
|
63
|
+
system: Math.max(0, estimates.system ?? 0),
|
|
64
|
+
user: Math.max(0, estimates.user ?? 0),
|
|
65
|
+
assistant: Math.max(0, estimates.assistant ?? 0),
|
|
66
|
+
tool: Math.max(0, estimates.tool ?? estimates.tools ?? 0)
|
|
67
|
+
};
|
|
68
|
+
const scaled = scaleToPrompt(rawTokens, prompt);
|
|
69
|
+
raw = [
|
|
70
|
+
{ id: "cached", tokens: cacheRead },
|
|
71
|
+
{ id: "system", tokens: scaled.system },
|
|
72
|
+
{ id: "user", tokens: scaled.user },
|
|
73
|
+
{ id: "assistant", tokens: scaled.assistant },
|
|
74
|
+
{ id: "tool", tokens: scaled.tool },
|
|
75
|
+
{ id: "other", tokens: scaled.other },
|
|
76
|
+
{ id: "think", tokens: reasoning },
|
|
77
|
+
{ id: "out", tokens: output },
|
|
78
|
+
{ id: "reserved", tokens: reserved },
|
|
79
|
+
{ id: "free", tokens: free }
|
|
80
|
+
];
|
|
81
|
+
} else {
|
|
82
|
+
raw = [
|
|
83
|
+
{ id: "cached", tokens: cacheRead },
|
|
84
|
+
{ id: "user", tokens: estimates.user },
|
|
85
|
+
{ id: "tools", tokens: estimates.tools },
|
|
86
|
+
{ id: "system", tokens: Math.max(0, prompt - estimates.user - estimates.tools) },
|
|
87
|
+
{ id: "think", tokens: reasoning },
|
|
88
|
+
{ id: "out", tokens: output },
|
|
89
|
+
{ id: "reserved", tokens: reserved },
|
|
90
|
+
{ id: "free", tokens: free }
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
raw = [
|
|
95
|
+
{ id: "cached", tokens: cacheRead },
|
|
96
|
+
{ id: "prompt", tokens: prompt },
|
|
97
|
+
{ id: "think", tokens: reasoning },
|
|
98
|
+
{ id: "out", tokens: output },
|
|
99
|
+
{ id: "reserved", tokens: reserved },
|
|
100
|
+
{ id: "free", tokens: free }
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
const filtered = raw.filter((segment) => segment.tokens > 0 && !excluded(segment.id));
|
|
50
104
|
return {
|
|
51
105
|
used,
|
|
52
106
|
window,
|
|
53
107
|
percent: window > 0 ? Math.min(100, Math.round(used / window * 100)) : 0,
|
|
54
|
-
segments:
|
|
108
|
+
segments: filtered,
|
|
55
109
|
cost,
|
|
56
110
|
known: window > 0
|
|
57
111
|
};
|
|
@@ -76,8 +130,11 @@ var VALID_SEGMENT_IDS = [
|
|
|
76
130
|
"cached",
|
|
77
131
|
"user",
|
|
78
132
|
"tools",
|
|
133
|
+
"tool",
|
|
79
134
|
"system",
|
|
80
135
|
"prompt",
|
|
136
|
+
"assistant",
|
|
137
|
+
"other",
|
|
81
138
|
"think",
|
|
82
139
|
"out",
|
|
83
140
|
"reserved",
|
|
@@ -98,8 +155,11 @@ var SEGMENT_LABEL = {
|
|
|
98
155
|
cached: "c",
|
|
99
156
|
user: "u",
|
|
100
157
|
tools: "m",
|
|
158
|
+
tool: "m",
|
|
101
159
|
system: "s",
|
|
102
160
|
prompt: "p",
|
|
161
|
+
assistant: "a",
|
|
162
|
+
other: "x",
|
|
103
163
|
think: "t",
|
|
104
164
|
out: "o",
|
|
105
165
|
reserved: "r",
|
|
@@ -107,7 +167,7 @@ var SEGMENT_LABEL = {
|
|
|
107
167
|
};
|
|
108
168
|
var money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
109
169
|
var intFmt = new Intl.NumberFormat("en-US");
|
|
110
|
-
var
|
|
170
|
+
var compact = (n) => formatCompactTokens(n);
|
|
111
171
|
var plugin = {
|
|
112
172
|
id: "opencode-plugin-context",
|
|
113
173
|
tui: async (api, rawOptions) => {
|
|
@@ -144,27 +204,55 @@ var plugin = {
|
|
|
144
204
|
}
|
|
145
205
|
};
|
|
146
206
|
function collectEstimates(api, sessionId) {
|
|
207
|
+
let system = 0;
|
|
147
208
|
let user = 0;
|
|
148
|
-
let
|
|
149
|
-
|
|
209
|
+
let assistant = 0;
|
|
210
|
+
let tool = 0;
|
|
211
|
+
let systemPrompt;
|
|
212
|
+
const messages = [...api.state.session.messages(sessionId)];
|
|
213
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
214
|
+
const s = messages[i]?.system?.trim();
|
|
215
|
+
if (s) {
|
|
216
|
+
systemPrompt = s;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (systemPrompt) system += estimateTokens(systemPrompt);
|
|
221
|
+
for (const message of messages) {
|
|
150
222
|
const role = message.role;
|
|
151
223
|
try {
|
|
152
224
|
for (const part of api.state.part(message.id)) {
|
|
153
225
|
const p = part;
|
|
154
|
-
if (
|
|
155
|
-
user += estimateTokens(p.text ?? "");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
else if (
|
|
226
|
+
if (role === "user") {
|
|
227
|
+
if (p.type === "text") user += estimateTokens(p.text ?? "");
|
|
228
|
+
else if (p.type === "file") user += estimateTokens(p.source?.text?.value ?? "");
|
|
229
|
+
else if (p.type === "agent") user += estimateTokens(p.source?.value ?? "");
|
|
230
|
+
} else if (role === "assistant") {
|
|
231
|
+
if (p.type === "text" || p.type === "reasoning") {
|
|
232
|
+
assistant += estimateTokens(p.text ?? "");
|
|
233
|
+
} else if (p.type === "tool") {
|
|
234
|
+
const state = p.state;
|
|
235
|
+
if (!state) continue;
|
|
236
|
+
const inputKeys = state.input && typeof state.input === "object" ? Object.keys(state.input).length : 0;
|
|
237
|
+
const inputChars = inputKeys * 16;
|
|
238
|
+
let out = "";
|
|
239
|
+
if (state.status === "pending") out = state.raw ?? "";
|
|
240
|
+
else if (state.status === "completed") out = typeof state.output === "string" ? state.output : "";
|
|
241
|
+
else if (state.status === "error") out = typeof state.error === "string" ? state.error : "";
|
|
242
|
+
else {
|
|
243
|
+
if (typeof state.output === "string") out = state.output;
|
|
244
|
+
else if (typeof state.error === "string") out = state.error;
|
|
245
|
+
else if (typeof state.raw === "string") out = state.raw;
|
|
246
|
+
}
|
|
247
|
+
const combined = "x".repeat(inputChars) + out;
|
|
248
|
+
tool += estimateTokens(combined);
|
|
249
|
+
}
|
|
162
250
|
}
|
|
163
251
|
}
|
|
164
252
|
} catch {
|
|
165
253
|
}
|
|
166
254
|
}
|
|
167
|
-
return { user, tools };
|
|
255
|
+
return { system, user, assistant, tool, tools: tool };
|
|
168
256
|
}
|
|
169
257
|
function sessionUsage(api, sessionId, config) {
|
|
170
258
|
const messages = api.state.session.messages(sessionId);
|
|
@@ -211,30 +299,46 @@ function renderPanel(api, sessionId, config) {
|
|
|
211
299
|
] })
|
|
212
300
|
);
|
|
213
301
|
if (config.estimate) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
302
|
+
const cellsById = new Map(bar.map((cell) => [cell.id, cell.cells]));
|
|
303
|
+
const tokenById = new Map(usage.segments.map((segment) => [segment.id, segment.tokens]));
|
|
304
|
+
const row = (ids, startAt) => {
|
|
305
|
+
const items = ids.map((id) => ({ id, cells: cellsById.get(id) ?? 0, tokens: tokenById.get(id) ?? 0 })).filter((item) => item.cells > 0 || item.tokens > 0);
|
|
306
|
+
if (items.length === 0) return null;
|
|
307
|
+
let cumulative = startAt;
|
|
308
|
+
return /* @__PURE__ */ jsx("box", { flexDirection: "row", children: items.map((item) => {
|
|
309
|
+
const marginLeft = cumulative;
|
|
310
|
+
cumulative += item.cells;
|
|
311
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "row", marginLeft, children: [
|
|
312
|
+
/* @__PURE__ */ jsx("text", { fg: segmentColor(item.id, theme, true), children: "\u258D" }),
|
|
313
|
+
/* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
|
|
314
|
+
SEGMENT_LABEL[item.id],
|
|
315
|
+
compact(item.tokens)
|
|
316
|
+
] })
|
|
317
|
+
] });
|
|
318
|
+
}) });
|
|
224
319
|
};
|
|
225
|
-
const
|
|
226
|
-
const
|
|
320
|
+
const usedStart = 0;
|
|
321
|
+
const usedIds = ["cached", "system", "user", "assistant", "tool", "other", "think", "out"];
|
|
322
|
+
const budgetStart = usedIds.reduce((sum, id) => sum + (cellsById.get(id) ?? (id === "tool" ? cellsById.get("tools") ?? 0 : 0)), 0);
|
|
323
|
+
const usedLegend = row(usedIds, usedStart);
|
|
324
|
+
const budgetLegend = row(["reserved", "free"], budgetStart);
|
|
227
325
|
if (usedLegend) lines.push(usedLegend);
|
|
228
326
|
if (budgetLegend) lines.push(budgetLegend);
|
|
229
327
|
} else {
|
|
328
|
+
const tokenById = new Map(usage.segments.map((segment) => [segment.id, segment.tokens]));
|
|
329
|
+
let cumulative = 0;
|
|
230
330
|
lines.push(
|
|
231
|
-
/* @__PURE__ */ jsx("box", { flexDirection: "row",
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
331
|
+
/* @__PURE__ */ jsx("box", { flexDirection: "row", children: bar.map((cell) => {
|
|
332
|
+
const marginLeft = cumulative;
|
|
333
|
+
cumulative += cell.cells;
|
|
334
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "row", marginLeft, children: [
|
|
335
|
+
/* @__PURE__ */ jsx("text", { fg: segmentColor(cell.id, theme, false), children: "\u258D" }),
|
|
336
|
+
/* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
|
|
337
|
+
SEGMENT_LABEL[cell.id],
|
|
338
|
+
compact(tokenById.get(cell.id) ?? 0)
|
|
339
|
+
] })
|
|
340
|
+
] });
|
|
341
|
+
}) })
|
|
238
342
|
);
|
|
239
343
|
}
|
|
240
344
|
}
|
|
@@ -260,15 +364,23 @@ function segmentColor(id, theme, estimate) {
|
|
|
260
364
|
free: theme.text,
|
|
261
365
|
user: theme.accent,
|
|
262
366
|
tools: theme.accent,
|
|
263
|
-
|
|
367
|
+
tool: theme.accent,
|
|
368
|
+
system: theme.accent,
|
|
369
|
+
assistant: theme.accent,
|
|
370
|
+
other: theme.accent
|
|
264
371
|
};
|
|
265
372
|
if (!estimate) return base[id];
|
|
266
373
|
const est = {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
374
|
+
system: theme.info,
|
|
375
|
+
user: theme.success,
|
|
376
|
+
assistant: theme.secondary,
|
|
377
|
+
tool: theme.warning,
|
|
378
|
+
tools: theme.warning,
|
|
379
|
+
other: theme.textMuted,
|
|
380
|
+
cached: theme.success,
|
|
270
381
|
think: theme.secondary,
|
|
271
382
|
out: theme.text,
|
|
383
|
+
reserved: theme.textMuted,
|
|
272
384
|
free: theme.borderSubtle
|
|
273
385
|
};
|
|
274
386
|
return est[id] ?? base[id];
|
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.1.
|
|
4
|
+
"version": "1.1.2",
|
|
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": {
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"@opentui/solid": ">=0.2.9"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@opencode-ai/plugin": "1.18.
|
|
49
|
-
"@opentui/core": "0.5.
|
|
50
|
-
"@opentui/solid": "0.5.
|
|
48
|
+
"@opencode-ai/plugin": "1.18.23",
|
|
49
|
+
"@opentui/core": "0.5.8",
|
|
50
|
+
"@opentui/solid": "0.5.8",
|
|
51
51
|
"@types/node": "^26.2.0",
|
|
52
52
|
"esbuild": "^0.28.0",
|
|
53
53
|
"solid-js": "1.9.12",
|