opencode-plugin-context 1.1.1 → 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 +135 -39
  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);
@@ -224,14 +312,14 @@ function renderPanel(api, sessionId, config) {
224
312
  /* @__PURE__ */ jsx("text", { fg: segmentColor(item.id, theme, true), children: "\u258D" }),
225
313
  /* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
226
314
  SEGMENT_LABEL[item.id],
227
- compactFmt.format(item.tokens)
315
+ compact(item.tokens)
228
316
  ] })
229
317
  ] });
230
318
  }) });
231
319
  };
232
320
  const usedStart = 0;
233
- const usedIds = ["cached", "user", "tools", "system", "think", "out"];
234
- const budgetStart = usedIds.reduce((sum, id) => sum + (cellsById.get(id) ?? 0), 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);
235
323
  const usedLegend = row(usedIds, usedStart);
236
324
  const budgetLegend = row(["reserved", "free"], budgetStart);
237
325
  if (usedLegend) lines.push(usedLegend);
@@ -247,7 +335,7 @@ function renderPanel(api, sessionId, config) {
247
335
  /* @__PURE__ */ jsx("text", { fg: segmentColor(cell.id, theme, false), children: "\u258D" }),
248
336
  /* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
249
337
  SEGMENT_LABEL[cell.id],
250
- compactFmt.format(tokenById.get(cell.id) ?? 0)
338
+ compact(tokenById.get(cell.id) ?? 0)
251
339
  ] })
252
340
  ] });
253
341
  }) })
@@ -276,15 +364,23 @@ function segmentColor(id, theme, estimate) {
276
364
  free: theme.text,
277
365
  user: theme.accent,
278
366
  tools: theme.accent,
279
- system: theme.accent
367
+ tool: theme.accent,
368
+ system: theme.accent,
369
+ assistant: theme.accent,
370
+ other: theme.accent
280
371
  };
281
372
  if (!estimate) return base[id];
282
373
  const est = {
283
- user: theme.info,
284
- tools: theme.accent,
285
- 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,
286
381
  think: theme.secondary,
287
382
  out: theme.text,
383
+ reserved: theme.textMuted,
288
384
  free: theme.borderSubtle
289
385
  };
290
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.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.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",