opencode-plugin-context 1.0.0 → 1.1.1
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 +73 -18
- package/dist/tui.js +153 -79
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
An OpenCode **TUI plugin** that replaces the built-in sidebar context block with a
|
|
4
4
|
colored, segmented bar of the current session's **context-window usage**.
|
|
5
5
|
|
|
6
|
-
The whole bar is the model's context window.
|
|
7
|
-
**cached** prompt, **prompt** (uncached input incl.
|
|
8
|
-
(reasoning tokens), **output**, and the model's
|
|
9
|
-
plus the numbers you already track: total tokens
|
|
6
|
+
The whole bar is the model's context window. By default it shows the real
|
|
7
|
+
provider-reported buckets — **cached** prompt, **prompt** (uncached input incl.
|
|
8
|
+
cache writes), **thinking** (reasoning tokens), **output**, and the model's
|
|
9
|
+
**reserved output** headroom — plus the numbers you already track: total tokens
|
|
10
|
+
used and money spent.
|
|
10
11
|
|
|
11
12
|
```
|
|
12
13
|
Context
|
|
13
14
|
━━━━━━━━━━━━━━━━━ 69%
|
|
14
|
-
|
|
15
|
+
138K / 200K tokens
|
|
15
16
|
$0.04 spent
|
|
16
|
-
▍
|
|
17
|
+
▍c40K ▍p90K ▍t5K ▍o3K ▍r6K ▍f62K
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
One color-coded legend row follows the bar — `▍` marker in the segment's color,
|
|
@@ -22,18 +23,67 @@ then a muted letter + count. Colors follow the active theme:
|
|
|
22
23
|
| Segment | Legend | Theme color | Default look |
|
|
23
24
|
| ------------------ | ------ | ----------- | --------------------- |
|
|
24
25
|
| cached input | `c` | `success` | green |
|
|
25
|
-
| prompt (uncached input, incl. cache writes) | `p` | `accent` | blue
|
|
26
|
+
| prompt (uncached input, incl. cache writes) | `p` | `accent` | blue |
|
|
26
27
|
| thinking (reasoning tokens) | `t` | `warning` | amber |
|
|
27
28
|
| output | `o` | `info` | cyan |
|
|
28
29
|
| reserved output | `r` | `textMuted` | grey |
|
|
29
30
|
| free space | `f` | `text` | white / default text |
|
|
30
31
|
|
|
32
|
+
With `estimate: true` the `prompt` bucket is split into **user** input, **tool**
|
|
33
|
+
calls + results (incl. MCP) and **system** (the remainder), shown as two legend
|
|
34
|
+
rows (used buckets, then reserved/free):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Context
|
|
38
|
+
━━━━━━━━━━━━━━━━━ 69%
|
|
39
|
+
▍c40K ▍u25K ▍m15K ▍s70K ▍t5K ▍o3K
|
|
40
|
+
▍r6K ▍f62K
|
|
41
|
+
138K / 200K tokens
|
|
42
|
+
$0.04 spent
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Segment | Legend | Theme color | Default look |
|
|
46
|
+
| ------------------ | ------ | ----------- | --------------------- |
|
|
47
|
+
| cached input | `c` | `success` | green |
|
|
48
|
+
| user input (est.) | `u` | `info` | cyan |
|
|
49
|
+
| tool calls + results (est., incl. MCP) | `m` | `accent` | blue |
|
|
50
|
+
| system prompt + tool definitions (rest) | `s` | `warning` | amber |
|
|
51
|
+
| thinking (reasoning tokens) | `t` | `secondary` | purple |
|
|
52
|
+
| output | `o` | `text` | white |
|
|
53
|
+
| reserved output | `r` | `textMuted` | grey |
|
|
54
|
+
| free space | `f` | `borderSubtle` | faint |
|
|
55
|
+
|
|
56
|
+
`u`/`m` are **estimates** — opencode's own chars/4 heuristic
|
|
57
|
+
(`Token.estimate`, used for compaction) applied to the visible message parts.
|
|
58
|
+
`c`/`s`/`t`/`o` are the real provider-reported buckets. `s` is the remainder of
|
|
59
|
+
the prompt bucket after `u` + `m` (the actual system prompt and tool definitions
|
|
60
|
+
aren't exposed by opencode's plugin API).
|
|
61
|
+
|
|
31
62
|
The bar spans the whole context window and fills the sidebar column: colored
|
|
32
|
-
cells for each segment in that order, then `free` fills the remainder
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
63
|
+
cells for each segment in that order, then `free` fills the remainder so the bar
|
|
64
|
+
always reaches full width. A very small segment may not fill a single bar cell
|
|
65
|
+
(e.g. 137 tokens in a 200K window is 0.07% of the bar) — its exact count is
|
|
66
|
+
always visible in the legend. Percent is colored like the usage plugin: green
|
|
67
|
+
`<50%`, amber `50–74%`, orange `75–99%`, red `100%`.
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
All options are optional. Plugin entry in `tui.json`:
|
|
72
|
+
|
|
73
|
+
```jsonc
|
|
74
|
+
{
|
|
75
|
+
"plugin_enabled": { "internal:sidebar-context": false },
|
|
76
|
+
"plugin": [["./context/tui.js", { "estimate": true, "exclude": ["system", "cached"] }]]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Option | Default | Description |
|
|
81
|
+
| ---------- | ------- | ---------------------------------------------------------- |
|
|
82
|
+
| `estimate` | `false` | split the prompt into `u`/`m`/`s` using char-count estimates. `true` replaces the single `p` (prompt) bucket with the estimated user/tool/system bars |
|
|
83
|
+
| `exclude` | `[]` | segment ids to drop from the bar + legend: `cached`, `user`, `tools`, `system`, `prompt`, `think`, `out`, `reserved`, `free` |
|
|
84
|
+
|
|
85
|
+
Excluded segments are removed from the visualization only; the tokens/cost and
|
|
86
|
+
percent lines still report the real totals.
|
|
37
87
|
|
|
38
88
|
## Requirements
|
|
39
89
|
|
|
@@ -91,17 +141,22 @@ Then register it in `~/.config/opencode/tui.json` and restart OpenCode:
|
|
|
91
141
|
`limit.context` / `limit.output` from `api.state.provider` — the same source
|
|
92
142
|
the built-in block uses.
|
|
93
143
|
- **used** = input + output + reasoning + cache.read + cache.write (opencode's
|
|
94
|
-
own total). Segments are never double-counted; `cache.write` folds into
|
|
95
|
-
|
|
144
|
+
own total). Segments are never double-counted; `cache.write` folds into the
|
|
145
|
+
prompt bucket, and **reserved output** shrinks as actual output grows.
|
|
146
|
+
- With `estimate: true`, the prompt bucket is split into `u`/`m`/`s` from
|
|
147
|
+
visible message parts; `s` is the remainder.
|
|
96
148
|
- Cost comes from `session.cost`, falling back to summing assistant `cost`.
|
|
97
149
|
- Repaints on `message.*` / `session.*` events plus a 2-second self-heal timer.
|
|
98
150
|
|
|
99
151
|
## What it deliberately does *not* show
|
|
100
152
|
|
|
101
|
-
The
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
153
|
+
The real system prompt and tool definitions are built server-side and are **not**
|
|
154
|
+
exposed through opencode's plugin API — only the aggregate token buckets and the
|
|
155
|
+
visible message parts are. The `u`/`m` split is therefore estimated from visible
|
|
156
|
+
parts (chars/4, the same heuristic opencode itself uses for compaction), `s` is
|
|
157
|
+
the remainder, and neither is a true token count the way GitHub Copilot's
|
|
158
|
+
context meter reports it. The `c`/`s`/`t`/`o`/`r` buckets come straight from the
|
|
159
|
+
provider's reported usage.
|
|
105
160
|
|
|
106
161
|
## Development
|
|
107
162
|
|
package/dist/tui.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// src/tui.
|
|
2
|
-
import { createElement, insert, setProp } from "@opentui/solid";
|
|
1
|
+
// src/tui.tsx
|
|
3
2
|
import { createTextAttributes } from "@opentui/core";
|
|
4
3
|
import { createSignal } from "solid-js";
|
|
5
4
|
|
|
@@ -21,15 +20,28 @@ function record(v) {
|
|
|
21
20
|
function num(v) {
|
|
22
21
|
return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.round(v) : 0;
|
|
23
22
|
}
|
|
24
|
-
function
|
|
23
|
+
function estimateTokens(input) {
|
|
24
|
+
return Math.max(0, Math.round(input.length / 4));
|
|
25
|
+
}
|
|
26
|
+
function computeContext(counts, limits, cost = 0, estimates, exclude = []) {
|
|
25
27
|
const { input, output, reasoning, cacheRead, cacheWrite } = counts;
|
|
26
28
|
const used = input + cacheRead + cacheWrite + reasoning + output;
|
|
27
29
|
const window = limits && limits.context > 0 ? limits.context : 0;
|
|
28
30
|
const reserved = limits && limits.output > 0 ? Math.max(0, limits.output - output) : 0;
|
|
29
31
|
const free = window > 0 ? Math.max(0, window - used - reserved) : 0;
|
|
30
|
-
const
|
|
32
|
+
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
|
+
] : [
|
|
31
43
|
{ id: "cached", tokens: cacheRead },
|
|
32
|
-
{ id: "prompt", tokens:
|
|
44
|
+
{ id: "prompt", tokens: prompt },
|
|
33
45
|
{ id: "think", tokens: reasoning },
|
|
34
46
|
{ id: "out", tokens: output },
|
|
35
47
|
{ id: "reserved", tokens: reserved },
|
|
@@ -39,33 +51,54 @@ function computeContext(counts, limits, cost = 0) {
|
|
|
39
51
|
used,
|
|
40
52
|
window,
|
|
41
53
|
percent: window > 0 ? Math.min(100, Math.round(used / window * 100)) : 0,
|
|
42
|
-
segments: raw.filter((segment) => segment.tokens > 0),
|
|
54
|
+
segments: raw.filter((segment) => segment.tokens > 0 && !exclude.includes(segment.id)),
|
|
43
55
|
cost,
|
|
44
56
|
known: window > 0
|
|
45
57
|
};
|
|
46
58
|
}
|
|
47
|
-
function segmentBar(segments, window, width) {
|
|
59
|
+
function segmentBar(segments, window, width, exclude = []) {
|
|
48
60
|
if (window <= 0 || width <= 0) return [];
|
|
49
61
|
let remaining = width;
|
|
50
62
|
const out = [];
|
|
51
|
-
|
|
52
|
-
for (; i < segments.length; i++) {
|
|
53
|
-
const segment = segments[i];
|
|
63
|
+
for (const segment of segments) {
|
|
54
64
|
if (segment.id === "free") break;
|
|
55
65
|
const cells = Math.min(remaining, Math.round(segment.tokens / window * width));
|
|
56
66
|
if (cells > 0) out.push({ id: segment.id, cells });
|
|
57
67
|
remaining -= cells;
|
|
58
68
|
}
|
|
59
|
-
if (remaining > 0) out.push({ id: "free", cells: remaining });
|
|
69
|
+
if (remaining > 0 && !exclude.includes("free")) out.push({ id: "free", cells: remaining });
|
|
60
70
|
return out;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
// src/tui.
|
|
73
|
+
// src/tui.tsx
|
|
74
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
75
|
+
var VALID_SEGMENT_IDS = [
|
|
76
|
+
"cached",
|
|
77
|
+
"user",
|
|
78
|
+
"tools",
|
|
79
|
+
"system",
|
|
80
|
+
"prompt",
|
|
81
|
+
"think",
|
|
82
|
+
"out",
|
|
83
|
+
"reserved",
|
|
84
|
+
"free"
|
|
85
|
+
];
|
|
86
|
+
function normalizeOptions(raw) {
|
|
87
|
+
const obj = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
88
|
+
const exclude = Array.isArray(obj.exclude) ? Array.from(new Set(obj.exclude.filter((id) => typeof id === "string" && VALID_SEGMENT_IDS.includes(id)))) : [];
|
|
89
|
+
return {
|
|
90
|
+
estimate: typeof obj.estimate === "boolean" ? obj.estimate : false,
|
|
91
|
+
exclude
|
|
92
|
+
};
|
|
93
|
+
}
|
|
64
94
|
var BAR_WIDTH = 32;
|
|
65
95
|
var BOLD = createTextAttributes({ bold: true });
|
|
66
96
|
var SLOT_ORDER = 60;
|
|
67
97
|
var SEGMENT_LABEL = {
|
|
68
98
|
cached: "c",
|
|
99
|
+
user: "u",
|
|
100
|
+
tools: "m",
|
|
101
|
+
system: "s",
|
|
69
102
|
prompt: "p",
|
|
70
103
|
think: "t",
|
|
71
104
|
out: "o",
|
|
@@ -73,9 +106,12 @@ var SEGMENT_LABEL = {
|
|
|
73
106
|
free: "f"
|
|
74
107
|
};
|
|
75
108
|
var money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
109
|
+
var intFmt = new Intl.NumberFormat("en-US");
|
|
110
|
+
var compactFmt = new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 });
|
|
76
111
|
var plugin = {
|
|
77
112
|
id: "opencode-plugin-context",
|
|
78
|
-
tui: async (api) => {
|
|
113
|
+
tui: async (api, rawOptions) => {
|
|
114
|
+
const config = normalizeOptions(rawOptions);
|
|
79
115
|
const [getRenderTick, setRenderTick] = createSignal(0);
|
|
80
116
|
const repaint = () => {
|
|
81
117
|
setRenderTick((n) => n + 1);
|
|
@@ -101,13 +137,36 @@ var plugin = {
|
|
|
101
137
|
slots: {
|
|
102
138
|
sidebar_content(_ctx, props) {
|
|
103
139
|
getRenderTick();
|
|
104
|
-
return renderPanel(api, props.session_id);
|
|
140
|
+
return renderPanel(api, props.session_id, config);
|
|
105
141
|
}
|
|
106
142
|
}
|
|
107
143
|
});
|
|
108
144
|
}
|
|
109
145
|
};
|
|
110
|
-
function
|
|
146
|
+
function collectEstimates(api, sessionId) {
|
|
147
|
+
let user = 0;
|
|
148
|
+
let tools = 0;
|
|
149
|
+
for (const message of api.state.session.messages(sessionId)) {
|
|
150
|
+
const role = message.role;
|
|
151
|
+
try {
|
|
152
|
+
for (const part of api.state.part(message.id)) {
|
|
153
|
+
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);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { user, tools };
|
|
168
|
+
}
|
|
169
|
+
function sessionUsage(api, sessionId, config) {
|
|
111
170
|
const messages = api.state.session.messages(sessionId);
|
|
112
171
|
let last;
|
|
113
172
|
for (const message of messages) {
|
|
@@ -134,62 +193,101 @@ function sessionUsage(api, sessionId) {
|
|
|
134
193
|
limits = { context: model.limit.context, output: model.limit.output ?? 0 };
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
|
-
|
|
196
|
+
const estimates = config.estimate ? collectEstimates(api, sessionId) : void 0;
|
|
197
|
+
return computeContext(counts, limits, cost, estimates, config.exclude);
|
|
138
198
|
}
|
|
139
|
-
function renderPanel(api, sessionId) {
|
|
199
|
+
function renderPanel(api, sessionId, config) {
|
|
140
200
|
const theme = api.theme.current;
|
|
141
|
-
const usage = sessionUsage(api, sessionId);
|
|
142
|
-
const header =
|
|
201
|
+
const usage = sessionUsage(api, sessionId, config);
|
|
202
|
+
const header = /* @__PURE__ */ jsx("text", { fg: theme.text, attributes: BOLD, children: "Context" });
|
|
143
203
|
const lines = [header];
|
|
144
204
|
const hasUsage = usage.used > 0;
|
|
145
205
|
if (usage.known && hasUsage) {
|
|
146
|
-
const bar = segmentBar(usage.segments, usage.window, BAR_WIDTH);
|
|
206
|
+
const bar = segmentBar(usage.segments, usage.window, BAR_WIDTH, config.exclude);
|
|
147
207
|
lines.push(
|
|
148
|
-
box
|
|
149
|
-
box
|
|
150
|
-
text
|
|
151
|
-
])
|
|
208
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", justifyContent: "space-between", children: [
|
|
209
|
+
/* @__PURE__ */ jsx("box", { flexDirection: "row", children: bar.map((cell) => /* @__PURE__ */ jsx("text", { fg: segmentColor(cell.id, theme, config.estimate), children: "\u2501".repeat(cell.cells) })) }),
|
|
210
|
+
/* @__PURE__ */ jsx("text", { fg: tierColor(usage.percent, theme), children: ` ${usage.percent}%` })
|
|
211
|
+
] })
|
|
152
212
|
);
|
|
213
|
+
if (config.estimate) {
|
|
214
|
+
const cellsById = new Map(bar.map((cell) => [cell.id, cell.cells]));
|
|
215
|
+
const tokenById = new Map(usage.segments.map((segment) => [segment.id, segment.tokens]));
|
|
216
|
+
const row = (ids, startAt) => {
|
|
217
|
+
const items = ids.map((id) => ({ id, cells: cellsById.get(id) ?? 0, tokens: tokenById.get(id) ?? 0 })).filter((item) => item.cells > 0 || item.tokens > 0);
|
|
218
|
+
if (items.length === 0) return null;
|
|
219
|
+
let cumulative = startAt;
|
|
220
|
+
return /* @__PURE__ */ jsx("box", { flexDirection: "row", children: items.map((item) => {
|
|
221
|
+
const marginLeft = cumulative;
|
|
222
|
+
cumulative += item.cells;
|
|
223
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "row", marginLeft, children: [
|
|
224
|
+
/* @__PURE__ */ jsx("text", { fg: segmentColor(item.id, theme, true), children: "\u258D" }),
|
|
225
|
+
/* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
|
|
226
|
+
SEGMENT_LABEL[item.id],
|
|
227
|
+
compactFmt.format(item.tokens)
|
|
228
|
+
] })
|
|
229
|
+
] });
|
|
230
|
+
}) });
|
|
231
|
+
};
|
|
232
|
+
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);
|
|
235
|
+
const usedLegend = row(usedIds, usedStart);
|
|
236
|
+
const budgetLegend = row(["reserved", "free"], budgetStart);
|
|
237
|
+
if (usedLegend) lines.push(usedLegend);
|
|
238
|
+
if (budgetLegend) lines.push(budgetLegend);
|
|
239
|
+
} else {
|
|
240
|
+
const tokenById = new Map(usage.segments.map((segment) => [segment.id, segment.tokens]));
|
|
241
|
+
let cumulative = 0;
|
|
242
|
+
lines.push(
|
|
243
|
+
/* @__PURE__ */ jsx("box", { flexDirection: "row", children: bar.map((cell) => {
|
|
244
|
+
const marginLeft = cumulative;
|
|
245
|
+
cumulative += cell.cells;
|
|
246
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "row", marginLeft, children: [
|
|
247
|
+
/* @__PURE__ */ jsx("text", { fg: segmentColor(cell.id, theme, false), children: "\u258D" }),
|
|
248
|
+
/* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
|
|
249
|
+
SEGMENT_LABEL[cell.id],
|
|
250
|
+
compactFmt.format(tokenById.get(cell.id) ?? 0)
|
|
251
|
+
] })
|
|
252
|
+
] });
|
|
253
|
+
}) })
|
|
254
|
+
);
|
|
255
|
+
}
|
|
153
256
|
}
|
|
154
257
|
if (hasUsage) {
|
|
155
258
|
lines.push(
|
|
156
|
-
text
|
|
157
|
-
`${formatInt(usage.used)} / ${usage.known ? formatInt(usage.window) : "--"} tokens`
|
|
158
|
-
])
|
|
259
|
+
/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: `${intFmt.format(usage.used)} / ${usage.known ? intFmt.format(usage.window) : "--"} tokens` })
|
|
159
260
|
);
|
|
160
261
|
} else {
|
|
161
|
-
lines.push(text
|
|
262
|
+
lines.push(/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: "no assistant turns yet" }));
|
|
162
263
|
}
|
|
163
264
|
if (usage.cost > 0) {
|
|
164
|
-
lines.push(text
|
|
165
|
-
}
|
|
166
|
-
if (usage.known && hasUsage) {
|
|
167
|
-
lines.push(
|
|
168
|
-
box({ flexDirection: "row", gap: 1 }, usage.segments.map(
|
|
169
|
-
(segment) => box({ flexDirection: "row" }, [
|
|
170
|
-
text({ fg: segmentColor(segment.id, theme) }, ["\u258D"]),
|
|
171
|
-
text({ fg: theme.textMuted }, [`${SEGMENT_LABEL[segment.id]}${formatCompact(segment.tokens)}`])
|
|
172
|
-
])
|
|
173
|
-
))
|
|
174
|
-
);
|
|
265
|
+
lines.push(/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: `${money.format(usage.cost)} spent` }));
|
|
175
266
|
}
|
|
176
|
-
return box
|
|
267
|
+
return /* @__PURE__ */ jsx("box", { width: "100%", flexDirection: "column", children: lines });
|
|
177
268
|
}
|
|
178
|
-
function segmentColor(id, theme) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
269
|
+
function segmentColor(id, theme, estimate) {
|
|
270
|
+
const base = {
|
|
271
|
+
cached: theme.success,
|
|
272
|
+
prompt: theme.accent,
|
|
273
|
+
think: theme.warning,
|
|
274
|
+
out: theme.info,
|
|
275
|
+
reserved: theme.textMuted,
|
|
276
|
+
free: theme.text,
|
|
277
|
+
user: theme.accent,
|
|
278
|
+
tools: theme.accent,
|
|
279
|
+
system: theme.accent
|
|
280
|
+
};
|
|
281
|
+
if (!estimate) return base[id];
|
|
282
|
+
const est = {
|
|
283
|
+
user: theme.info,
|
|
284
|
+
tools: theme.accent,
|
|
285
|
+
system: theme.warning,
|
|
286
|
+
think: theme.secondary,
|
|
287
|
+
out: theme.text,
|
|
288
|
+
free: theme.borderSubtle
|
|
289
|
+
};
|
|
290
|
+
return est[id] ?? base[id];
|
|
193
291
|
}
|
|
194
292
|
function tierColor(percent, theme) {
|
|
195
293
|
if (percent >= 100) return theme.error;
|
|
@@ -197,30 +295,6 @@ function tierColor(percent, theme) {
|
|
|
197
295
|
if (percent >= 50) return theme.accent;
|
|
198
296
|
return theme.success;
|
|
199
297
|
}
|
|
200
|
-
function formatInt(value) {
|
|
201
|
-
return new Intl.NumberFormat("en-US").format(value);
|
|
202
|
-
}
|
|
203
|
-
function formatCompact(value) {
|
|
204
|
-
if (value >= 1e6) return `${(value / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
|
|
205
|
-
if (value >= 1e3) return `${Math.round(value / 1e3)}k`;
|
|
206
|
-
return String(value);
|
|
207
|
-
}
|
|
208
|
-
function element(tag, props, children = []) {
|
|
209
|
-
const node = createElement(tag);
|
|
210
|
-
for (const [key, value] of Object.entries(props)) {
|
|
211
|
-
if (value !== void 0) setProp(node, key, value);
|
|
212
|
-
}
|
|
213
|
-
for (const child of children) {
|
|
214
|
-
if (child !== null && child !== void 0 && child !== false) insert(node, child);
|
|
215
|
-
}
|
|
216
|
-
return node;
|
|
217
|
-
}
|
|
218
|
-
function text(props, children = []) {
|
|
219
|
-
return element("text", props, children);
|
|
220
|
-
}
|
|
221
|
-
function box(props, children = []) {
|
|
222
|
-
return element("box", props, children);
|
|
223
|
-
}
|
|
224
298
|
var tui_default = plugin;
|
|
225
299
|
export {
|
|
226
300
|
tui_default as default
|
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.1.1",
|
|
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": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"opencode": ">=1.18.0"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "esbuild src/tui.
|
|
19
|
+
"build": "esbuild src/tui.tsx --bundle --format=esm --platform=node --jsx=automatic --jsx-import-source=@opentui/solid --external:@opencode-ai/plugin --external:@opentui/solid --external:@opentui/core --external:solid-js --outfile=dist/tui.js",
|
|
20
20
|
"dev:install": "npm run build && mkdir -p \"$HOME/.config/opencode/context\" && cp dist/tui.js \"$HOME/.config/opencode/context/tui.js\"",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
22
|
"test": "node scripts/check.mjs",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opencode-ai/plugin": "1.18.18",
|
|
49
49
|
"@opentui/core": "0.5.4",
|
|
50
|
-
"@opentui/keymap": "0.5.4",
|
|
51
50
|
"@opentui/solid": "0.5.4",
|
|
52
51
|
"@types/node": "^26.2.0",
|
|
53
52
|
"esbuild": "^0.28.0",
|