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.
Files changed (2) hide show
  1. package/dist/tui.js +166 -54
  2. 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.round(input.length / 4));
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 raw = estimates ? [
34
- { id: "cached", tokens: cacheRead },
35
- { id: "user", tokens: estimates.user },
36
- { id: "tools", tokens: estimates.tools },
37
- { id: "system", tokens: Math.max(0, prompt - estimates.user - estimates.tools) },
38
- { id: "think", tokens: reasoning },
39
- { id: "out", tokens: output },
40
- { id: "reserved", tokens: reserved },
41
- { id: "free", tokens: free }
42
- ] : [
43
- { id: "cached", tokens: cacheRead },
44
- { id: "prompt", tokens: prompt },
45
- { id: "think", tokens: reasoning },
46
- { id: "out", tokens: output },
47
- { id: "reserved", tokens: reserved },
48
- { id: "free", tokens: free }
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: raw.filter((segment) => segment.tokens > 0 && !exclude.includes(segment.id)),
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 compactFmt = new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 });
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 tools = 0;
149
- for (const message of api.state.session.messages(sessionId)) {
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 (p.type === "text" && role === "user") {
155
- user += estimateTokens(p.text ?? "");
156
- } else if (p.type === "tool") {
157
- const state = p.state;
158
- if (!state) continue;
159
- if (state.input !== void 0) tools += estimateTokens(JSON.stringify(state.input));
160
- if (typeof state.output === "string") tools += estimateTokens(state.output);
161
- else if (typeof state.error === "string") tools += estimateTokens(state.error);
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 legend = (ids) => {
215
- const entries = usage.segments.filter((segment) => ids.includes(segment.id));
216
- if (entries.length === 0) return null;
217
- return /* @__PURE__ */ jsx("box", { flexDirection: "row", children: entries.map((segment) => /* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
218
- /* @__PURE__ */ jsx("text", { fg: segmentColor(segment.id, theme, true), children: "\u258D" }),
219
- /* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
220
- SEGMENT_LABEL[segment.id],
221
- compactFmt.format(segment.tokens)
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 usedLegend = legend(["cached", "user", "tools", "system", "think", "out"]);
226
- const budgetLegend = legend(["reserved", "free"]);
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", gap: 1, children: usage.segments.map((segment) => /* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
232
- /* @__PURE__ */ jsx("text", { fg: segmentColor(segment.id, theme, false), children: "\u258D" }),
233
- /* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
234
- SEGMENT_LABEL[segment.id],
235
- compactFmt.format(segment.tokens)
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
- system: theme.accent
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
- user: theme.info,
268
- tools: theme.accent,
269
- system: theme.warning,
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.0",
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.18",
49
- "@opentui/core": "0.5.4",
50
- "@opentui/solid": "0.5.4",
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",