opencode-context-indicator 1.7.1 → 1.8.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/CHANGELOG.md +57 -0
- package/README.md +97 -94
- package/index.js +5 -4
- package/lib/contributors.js +5 -4
- package/lib/limits.js +19 -7
- package/lib/state.js +6 -5
- package/lib/tui-data.d.ts +0 -1
- package/lib/tui-data.js +5 -21
- package/lib/tui-h.ts +73 -0
- package/lib/{tui-panel.tsx → tui-panel.ts} +779 -713
- package/lib/tui-sidebar.ts +153 -0
- package/package.json +10 -12
- package/tui.ts +317 -0
- package/lib/tui-sidebar.tsx +0 -76
- package/tui.tsx +0 -206
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,63 @@ Notable user-facing changes per release. Dates are commit days (UTC).
|
|
|
4
4
|
The package also ships a dual V1/V2 plugin — see the README for the surface
|
|
5
5
|
matrix; everything below concerns the V2 path unless stated otherwise.
|
|
6
6
|
|
|
7
|
+
## 1.8.0 — 2026-10-31
|
|
8
|
+
|
|
9
|
+
- **The TUI sidebar and the `/cx` panel are live for npm installs too**
|
|
10
|
+
(workaround for upstream anomalyco/opencode#33884). OpenCode skips its
|
|
11
|
+
Solid/Babel JSX transform for any file inside a `node_modules` directory, so a
|
|
12
|
+
JSX-built slot mounted from an npm install rendered once and then froze on
|
|
13
|
+
"no data yet"; 1.7.x avoided that by disabling the surfaces entirely. The TUI
|
|
14
|
+
tree is now built with `jsx()` plus getter props (`lib/tui-h.ts`), which needs
|
|
15
|
+
no transform at all: the host rewrites the literal
|
|
16
|
+
`@opentui/solid/jsx-runtime` import to its own in-process module, so the
|
|
17
|
+
surfaces stay reactive from any install path. The npm-path auto-disable and its
|
|
18
|
+
console hint are gone; `file://` installs behave exactly as before.
|
|
19
|
+
- Relative imports of `.ts` files now carry the extension (`./lib/tui-h.ts`):
|
|
20
|
+
the host decides which files get the runtime rewrite by walking the import
|
|
21
|
+
graph with `import.meta.resolve`, which never probes extensions. An
|
|
22
|
+
extensionless import silently kept the literal `@opentui/solid` specifier
|
|
23
|
+
and failed with `Cannot find package '@opentui/solid'` - gate (b) now
|
|
24
|
+
rejects it.
|
|
25
|
+
- **No runtime dependencies at all.** `@opentui/solid` moved from
|
|
26
|
+
`dependencies` to `devDependencies` (types + local typecheck only, still
|
|
27
|
+
pinned to an exact version). A fresh install drops from ~28 MB / ~800 files to
|
|
28
|
+
a 147 KB tarball (27 files, ~419 KB unpacked), and the duplicate
|
|
29
|
+
`@opentui/core`
|
|
30
|
+
/ Babel toolchain
|
|
31
|
+
(`@babel/core`, `babel-plugin-module-resolver`, `babel-preset-solid`, `s-js`,
|
|
32
|
+
`entities`) no longer lands in the opencode cache next to the host's own copy.
|
|
33
|
+
- **Baseline: opencode 2.2.5 / OpenTUI 0.8.** Dev dependencies, the peer floor
|
|
34
|
+
and the typecheck now target `@opentui/solid` + `@opentui/core` `0.8.0` and
|
|
35
|
+
`@opencode/plugin` `2.2.5` (opencode 2.2.3 made OpenTUI 0.8 mandatory for TUI
|
|
36
|
+
plugins; 2.2.5 is the latest release at the time of writing and still ships
|
|
37
|
+
0.8.0). `jsx()`/getter semantics are unchanged in 0.8 - `spreadExpression`
|
|
38
|
+
still reads every prop inside a `createRenderEffect` - so the JSX-free tree
|
|
39
|
+
behaves exactly as before; the whole gate set (types, fast gate, Check C) was
|
|
40
|
+
re-run on a real 2.2.5 host. A host that still ships OpenTUI 0.7 (opencode
|
|
41
|
+
2.2.0-2.2.2) may not load the TUI surfaces.
|
|
42
|
+
- **Denominator follows opencode 2.2.5.** The context window used for every
|
|
43
|
+
percentage is now `input && context ? min(input, context) : input || context`,
|
|
44
|
+
mirroring core's `inputWindow()` after #60660: a provider that reports an
|
|
45
|
+
input limit above the context window cannot use it in full, because the
|
|
46
|
+
context also holds the reply. Before this release an input limit above the
|
|
47
|
+
context window was used as-is, which overstated the window (and printed a
|
|
48
|
+
compact-at point the host never reaches). Covered by unit test T37. opencode
|
|
49
|
+
2.2.5 also accepts a percentage `compaction.buffer`; that setting is not
|
|
50
|
+
visible to plugins, so a percentage buffer still shows the default reserve.
|
|
51
|
+
- **Gates cover the failure mode end to end.** Fast gate (b) is rewritten: the
|
|
52
|
+
TUI surface must contain no `.tsx`/`.jsx` file and no `@jsx*` pragma, every
|
|
53
|
+
TUI source must parse with esbuild `loader:"ts"` (JSX syntax fails), and
|
|
54
|
+
imports must stay inside the host-bridged allowlist. Gate (c) now walks the
|
|
55
|
+
real import graph and fails if a host-bridged module is a production
|
|
56
|
+
dependency. New e2e **Check C**: `npm pack` → extract into a throwaway
|
|
57
|
+
`node_modules/opencode-context-indicator` → isolated `--standalone` TUI →
|
|
58
|
+
assert an env-gated getter-driven probe keeps re-rendering (a frozen tree
|
|
59
|
+
writes the same value forever). `node test/e2e-tui.mjs --check-c` runs it
|
|
60
|
+
alone, without an LLM call.
|
|
61
|
+
- **Docs:** the README no longer describes a `file://`-only live sidebar;
|
|
62
|
+
"Live sidebar" documents the JSX-free design and the size win.
|
|
63
|
+
|
|
7
64
|
## 1.7.1 — 2026-10-29
|
|
8
65
|
|
|
9
66
|
- **Fixed: the live log no longer prints `ctx=0` above real categories.** A hook
|
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ generations) plus an optional TUI module (`./tui` entry) that renders a **live
|
|
|
15
15
|
sidebar** and a **zero-LLM `/cx` breakdown panel** with measured contributors
|
|
16
16
|
and a redacted export, and an **experimental Desktop GUI part**
|
|
17
17
|
(`./gui-experimental`: a side-panel tab and a composer pill). The V2 path is
|
|
18
|
-
**tested against opencode 2.2.0** (the isolated load + state-write
|
|
19
|
-
re-run against the 2.2.
|
|
18
|
+
**tested against opencode 2.2.5 / OpenTUI 0.8** (the isolated load + state-write
|
|
19
|
+
e2e was also re-run against the 2.2.0-2.2.4 Desktop CLI); the V1 path against
|
|
20
|
+
1.18.29+.
|
|
20
21
|
|
|
21
22
|
> **V1 is feature-frozen.** It is kept working for compatibility (bug fixes
|
|
22
23
|
> only); all new features land in the V2 path.
|
|
@@ -32,7 +33,7 @@ re-run against the 2.2.1 and 2.2.2 Desktop CLI); the V1 path against 1.18.29+.
|
|
|
32
33
|
For every served request the plugin estimates how the context window is split:
|
|
33
34
|
|
|
34
35
|
```
|
|
35
|
-
[2026-…] session=ses_abc model=… ctx=45.2k/120k (
|
|
36
|
+
[2026-…] session=ses_abc model=… ctx=45.2k/120k (37.7%) count=45.2k usable=… limit=… maxOutput=… reserved=… input=44.1k
|
|
36
37
|
user 4.2k (est tokens)
|
|
37
38
|
assistant 3.1k (est tokens)
|
|
38
39
|
reasoning 0.8k (est tokens) (exact 1.2k)
|
|
@@ -42,7 +43,7 @@ For every served request the plugin estimates how the context window is split:
|
|
|
42
43
|
tool schemas 1.4k (est tokens)
|
|
43
44
|
other 5.6k (= input - sum of estimates)
|
|
44
45
|
source context-hook
|
|
45
|
-
prompt cache read=12.0k
|
|
46
|
+
prompt cache read=12.0k write=1.1k (provider-reported; subset of input)
|
|
46
47
|
reclaim ~9.8k redundant tool output (3 duplicate(s) — estimate)
|
|
47
48
|
subagents 2 session(s): in=8.1k out=2.3k r=1.0k worst=12%
|
|
48
49
|
```
|
|
@@ -61,7 +62,9 @@ For every served request the plugin estimates how the context window is split:
|
|
|
61
62
|
`input + output + cache.read + cache.write`. Percentages are **not clamped**
|
|
62
63
|
at 100% — exceeding the window stays visible.
|
|
63
64
|
* `prompt cache` is **provider-reported and exact** (`tokens.cache.read` /
|
|
64
|
-
`tokens.cache.write`)
|
|
65
|
+
`tokens.cache.write`). The live snapshot prints the raw read/write counts;
|
|
66
|
+
`/context` and `/context-breakdown` also append the read share of `input`
|
|
67
|
+
when the provider reported it. It is a *subset* of input,
|
|
65
68
|
so it is deliberately **not** added to the category sum: a cache read means the
|
|
66
69
|
provider served those prompt tokens from its prompt cache, not that they are
|
|
67
70
|
absent from the window. `n/a` when the provider reports nothing.
|
|
@@ -73,10 +76,11 @@ For every served request the plugin estimates how the context window is split:
|
|
|
73
76
|
— never as "the model wasted effort". Findings are per-session and **path-free
|
|
74
77
|
in the persisted/exported form**.
|
|
75
78
|
* The denominator is the **opencode 2.x compaction ceiling**: `window =
|
|
76
|
-
limit.input
|
|
79
|
+
limit.input && limit.context ? min(limit.input, limit.context) : limit.input
|
|
80
|
+
|| limit.context`; `reserve = max(floor(window * 0.15), window >=
|
|
77
81
|
2 * 16_000 ? 16_000 : 0)`; the ceiling is `window - reserve`. This mirrors
|
|
78
82
|
`calculateCeiling()` in `packages/core/src/session/compaction.ts`, whose
|
|
79
|
-
`RESERVE_SHARE` is 0.15 in every released host from 2.1.4 through 2.2.
|
|
83
|
+
`RESERVE_SHARE` is 0.15 in every released host from 2.1.4 through 2.2.5 (the
|
|
80
84
|
unreleased `v2` branch has a different share; the plugin follows the release,
|
|
81
85
|
not the branch). When opencode
|
|
82
86
|
carries a `compaction.buffer` configured in its own settings, the plugin
|
|
@@ -85,6 +89,9 @@ For every served request the plugin estimates how the context window is split:
|
|
|
85
89
|
to override the reserve for testing or custom setups (non-negative integer <=
|
|
86
90
|
1e9; invalid values are silently ignored; a buffer that is **not smaller than
|
|
87
91
|
the window** is ignored, exactly like core, so it can never leave zero room).
|
|
92
|
+
Since opencode 2.2.5 that setting also accepts a percentage (`"buffer": "20%"`),
|
|
93
|
+
which the plugin cannot read either, so a percentage buffer is **not**
|
|
94
|
+
reflected in the "compact at" line until it is mirrored from core.
|
|
88
95
|
|
|
89
96
|
### Toast (V1 only)
|
|
90
97
|
|
|
@@ -116,9 +123,9 @@ other 5.6k 12%
|
|
|
116
123
|
updated 12:30:01
|
|
117
124
|
```
|
|
118
125
|
|
|
119
|
-
>
|
|
120
|
-
>
|
|
121
|
-
> [Live sidebar
|
|
126
|
+
> Works on **every** install path, including the npm one — the TUI surfaces are
|
|
127
|
+
> built without JSX precisely so `node_modules` installs stay live. See
|
|
128
|
+
> [Live sidebar](#live-sidebar).
|
|
122
129
|
|
|
123
130
|
### Slash commands (V2)
|
|
124
131
|
|
|
@@ -127,13 +134,13 @@ commands:
|
|
|
127
134
|
|
|
128
135
|
| Command | Scope | Delivery | Agent turn? |
|
|
129
136
|
| --- | --- | --- | :---: |
|
|
130
|
-
| `/context` | **this session only** (no subagents); cache + `state.json` only | `resume:true`
|
|
137
|
+
| `/context` | **this session only** (no subagents); cache + `state.json` only | `resume:true` - the model echoes the short summary into the transcript | yes |
|
|
131
138
|
| `/context-breakdown` | this session **+ all subagent descendants** | `resume:true` — the model echoes the table into the transcript | yes |
|
|
132
139
|
| `/context-diff` | **this session only**; previous measurement vs current | `resume:true` — the model echoes the delta table into the transcript | yes |
|
|
133
140
|
|
|
134
141
|
#### `/context` — quick summary (main session only)
|
|
135
142
|
|
|
136
|
-
A
|
|
143
|
+
A short summary of the current session, built purely from the in-memory cache
|
|
137
144
|
and the `state.json` snapshot: **no subagent traversal and no live RPC**, so
|
|
138
145
|
collection is instant (delivery itself still runs an agent turn — see below).
|
|
139
146
|
The synthetic notice chip shows a self-contained one-liner (label =
|
|
@@ -157,6 +164,7 @@ instructions to follow.
|
|
|
157
164
|
<context-summary>
|
|
158
165
|
**Context** — your-model · 41.5k / 168k (25%)
|
|
159
166
|
usr 2.3k · asst 285 · rsn 9.6k · tool 2.1k · out 24.7k · sys 8.5k · schm 3.8k · oth 4.1k
|
|
167
|
+
compact at 143k (25.0k reserve)
|
|
160
168
|
updated 12:36:16 (snap)
|
|
161
169
|
</context-summary>
|
|
162
170
|
```
|
|
@@ -266,7 +274,8 @@ the content is visible in the Desktop (the chip only shows `description`).
|
|
|
266
274
|
`CONTEXT_BREAKDOWN_RESUME` controls `/context-breakdown` back to a plain
|
|
267
275
|
notice if needed.
|
|
268
276
|
|
|
269
|
-
> **E2E note (Desktop 2.0.19, local model `llama3.2:latest`,
|
|
277
|
+
> **E2E note (historical run on Desktop 2.0.19, local model `llama3.2:latest`,
|
|
278
|
+
> `build` agent).**
|
|
270
279
|
> `resume:true` behaved as designed: the synthetic item was drained into
|
|
271
280
|
> `/message` and a turn ran; the assistant message contained the table verbatim
|
|
272
281
|
> (the trailing `_…_` note line was dropped by the model). Because `resume:true`
|
|
@@ -362,9 +371,8 @@ carries its tool name); tool **result outputs** are counted here but not by the
|
|
|
362
371
|
server's categories — which is why the residual `other` can look large. Top 5
|
|
363
372
|
are shown. Empty state: `no messages yet`.
|
|
364
373
|
|
|
365
|
-
Live
|
|
366
|
-
|
|
367
|
-
`/context`, `/context-breakdown` and `/context-diff` instead.
|
|
374
|
+
Live on every install path (npm and `file://`). The Desktop has no TUI, so it
|
|
375
|
+
uses `/context`, `/context-breakdown` and `/context-diff` instead.
|
|
368
376
|
|
|
369
377
|
#### Export (redacted)
|
|
370
378
|
|
|
@@ -473,9 +481,8 @@ Notes — these reflect what the plugin API actually exposes today:
|
|
|
473
481
|
therefore delivers everything through the log file, the TUI sidebar, the
|
|
474
482
|
`/cx` session panel and the `/context` / `/context-breakdown` / `/context-diff` slash commands. The sidebar is a TUI-process
|
|
475
483
|
slot and does not exist in the Desktop app; the slash commands work in **both**.
|
|
476
|
-
The TUI sidebar is
|
|
477
|
-
|
|
478
|
-
[Live sidebar: npm vs file:// install](#live-sidebar-npm-vs-file-install).
|
|
484
|
+
The TUI sidebar is live on **every** install path, npm included — see
|
|
485
|
+
[Live sidebar](#live-sidebar).
|
|
479
486
|
* **V1 has no sidebar and no slash command.** The command/slot API (and the
|
|
480
487
|
`./tui` entry) is a V2 feature; OpenCode 1.x has no equivalent, so V1 is
|
|
481
488
|
toast + log only.
|
|
@@ -537,8 +544,8 @@ or add it to `opencode.jsonc`:
|
|
|
537
544
|
```
|
|
538
545
|
|
|
539
546
|
The `./tui` sidebar entry is loaded automatically alongside the main plugin in
|
|
540
|
-
the terminal TUI
|
|
541
|
-
|
|
547
|
+
the terminal TUI, on every install method — see [Live sidebar](#live-sidebar)
|
|
548
|
+
below.
|
|
542
549
|
For a CLI-only setup against remote servers, the package can also be listed in
|
|
543
550
|
[`cli.json`](https://opencode.ai/v2/docs/cli/plugins):
|
|
544
551
|
|
|
@@ -546,45 +553,38 @@ For a CLI-only setup against remote servers, the package can also be listed in
|
|
|
546
553
|
{ "plugins": ["opencode-context-indicator"] }
|
|
547
554
|
```
|
|
548
555
|
|
|
549
|
-
### Live sidebar
|
|
556
|
+
### Live sidebar
|
|
550
557
|
|
|
551
|
-
The TUI sidebar
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
skips the host Solid transform for any path inside a `node_modules` directory, so
|
|
555
|
-
a slot mounted from an npm install renders once and then never live-updates.
|
|
558
|
+
The TUI sidebar and the `/cx` panel are **TUI-process slots**, live on every
|
|
559
|
+
install path — npm (`opencode plugin add opencode-context-indicator`), `file://`
|
|
560
|
+
and the local `plugins/` copy alike.
|
|
556
561
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
/
|
|
566
|
-
every install.
|
|
567
|
-
|
|
568
|
-
#### Getting the live sidebar (`file://` install)
|
|
562
|
+
That used to be false: OpenCode skips its Solid/JSX transform for any file inside
|
|
563
|
+
a `node_modules` directory (upstream `anomalyco/opencode#33884`), so a JSX-built
|
|
564
|
+
slot mounted from an npm install rendered once and then froze on
|
|
565
|
+
"no data yet". Since **1.8.0** the plugin ships **no JSX at all**: the tree is
|
|
566
|
+
built with `jsx()` and getter props (`lib/tui-h.ts`), which needs no transform,
|
|
567
|
+
and the host rewrites the literal `@opentui/solid/jsx-runtime` import to its own
|
|
568
|
+
in-process module. The package therefore also declares **no runtime
|
|
569
|
+
dependencies** — nothing is installed next to it, so there is no second
|
|
570
|
+
Solid/OpenTUI instance to fight with.
|
|
569
571
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
(e.g. `C:\Users\you\plugins\...`, not `.../node_modules/...`).
|
|
575
|
-
2. Point OpenCode at that directory in `~/.config/opencode/opencode.jsonc`:
|
|
572
|
+
| Install | Desktop | TUI commands | TUI live sidebar | TUI panel (`/cx`) |
|
|
573
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
574
|
+
| npm | ✅ | ✅ | ✅ live | ✅ live |
|
|
575
|
+
| `file://` (any path) | ✅ | ✅ | ✅ live | ✅ live |
|
|
576
576
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"plugins": ["file:///C:/Users/you/plugins/opencode-context-indicator"]
|
|
580
|
-
}
|
|
581
|
-
```
|
|
577
|
+
`/context`, `/context-breakdown`, `/context-diff` and the breakdown log file work
|
|
578
|
+
on every install regardless.
|
|
582
579
|
|
|
583
|
-
|
|
580
|
+
> If a sidebar shows "no data yet" forever on 1.8.0+, that is a bug — please
|
|
581
|
+
> report it with the host version. On 1.7.x and older, the npm path was disabled
|
|
582
|
+
> by design; upgrading is the fix.
|
|
584
583
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
584
|
+
`test/e2e-tui.mjs` Check C proves this end to end: it runs `npm pack`, extracts
|
|
585
|
+
the tarball into a throwaway `node_modules/opencode-context-indicator`, boots an
|
|
586
|
+
isolated `--standalone` TUI from there and asserts that a getter-driven probe
|
|
587
|
+
node actually re-renders (a frozen tree writes the same value forever).
|
|
588
588
|
|
|
589
589
|
### OpenCode 1.x (V1)
|
|
590
590
|
|
|
@@ -601,25 +601,28 @@ Add the package name to the `plugin` array in your `opencode.json`:
|
|
|
601
601
|
Copy `index.js` and the whole `lib/` directory into
|
|
602
602
|
`~/.config/opencode/plugins/`, keeping the `lib/` subdirectory next to the
|
|
603
603
|
plugin file so the plugin's relative imports resolve. The `lib/` folder is
|
|
604
|
-
support code, not a separate plugin entry
|
|
604
|
+
support code, not a separate plugin entry; copy **every** module in it
|
|
605
|
+
(`cache.js`, `commands.js`, `contributors.js`, `dedup.js`, `desktop.js`,
|
|
606
|
+
`estimate.js`, `format.js`, `limits.js`, `rpc.js`, `state.js`, `tui-data.js`,
|
|
607
|
+
`tui-data.d.ts`, `tui-h.ts`, `tui-panel.ts`, `tui-sidebar.ts`, `tui-theme.js`,
|
|
608
|
+
`v1.js`, `v2.js`) — a
|
|
609
|
+
missing module only disables the surface that imports it, but the server part
|
|
610
|
+
stops loading if one of its own imports is absent:
|
|
605
611
|
|
|
606
612
|
```
|
|
607
|
-
~/.config/opencode/plugins/context-indicator.js
|
|
608
|
-
~/.config/opencode/plugins/lib
|
|
609
|
-
~/.config/opencode/plugins/lib
|
|
610
|
-
~/.config/opencode/plugins/lib/dedup.js
|
|
611
|
-
~/.config/opencode/plugins/lib/estimate.js
|
|
612
|
-
~/.config/opencode/plugins/lib/limits.js
|
|
613
|
-
~/.config/opencode/plugins/lib/state.js
|
|
614
|
-
~/.config/opencode/plugins/lib/v1.js
|
|
615
|
-
~/.config/opencode/plugins/lib/v2.js
|
|
613
|
+
~/.config/opencode/plugins/context-indicator.js (this package's index.js)
|
|
614
|
+
~/.config/opencode/plugins/lib/*.js
|
|
615
|
+
~/.config/opencode/plugins/lib/*.ts
|
|
616
616
|
```
|
|
617
617
|
|
|
618
|
+
`tui.ts` (the `./tui` entry, at the package root) is only needed for the
|
|
619
|
+
terminal TUI surfaces; it imports `lib/tui-h.ts`, `lib/tui-sidebar.ts` and
|
|
620
|
+
`lib/tui-panel.ts` by their `.ts` file names, so copy those alongside the `.js`
|
|
621
|
+
modules.
|
|
622
|
+
|
|
618
623
|
To use the sidebar locally as well, point OpenCode at a `file://` copy of the
|
|
619
|
-
package
|
|
620
|
-
[Live sidebar
|
|
621
|
-
is the only install path that keeps the sidebar live. An npm install auto-disables
|
|
622
|
-
the sidebar instead.
|
|
624
|
+
package — the sidebar is live there too, exactly as with an npm install (see
|
|
625
|
+
[Live sidebar](#live-sidebar)).
|
|
623
626
|
|
|
624
627
|
---
|
|
625
628
|
|
|
@@ -635,26 +638,22 @@ the sidebar instead.
|
|
|
635
638
|
indicator degrades
|
|
636
639
|
gracefully (the breakdown falls back to throttled `session.messages` fetches
|
|
637
640
|
and toasts keep working).
|
|
638
|
-
* **OpenCode ≥ 2.2.
|
|
639
|
-
against **2.2.
|
|
640
|
-
**
|
|
641
|
-
path keeps working on
|
|
642
|
-
2.0.16
|
|
643
|
-
so a host on OpenTUI 0.
|
|
644
|
-
|
|
641
|
+
* **OpenCode ≥ 2.2.3** for the V2 path with the TUI surfaces (built and verified
|
|
642
|
+
against **2.2.5**, the latest at the time of writing, which still ships OpenTUI
|
|
643
|
+
**0.8**; the isolated load + state-write e2e also passes on the 2.2.0-2.2.4
|
|
644
|
+
Desktop CLI). The server-only V2 path keeps working on
|
|
645
|
+
2.0.16+. The TUI module is typed against the OpenTUI generation the host ships,
|
|
646
|
+
so a host on an older OpenTUI (0.7 for opencode 2.2.0-2.2.2, 0.6 for
|
|
647
|
+
opencode ≤ 2.1.4) may not load the TUI surfaces - neither the sidebar nor the
|
|
648
|
+
`/cx` panel - from a 1.8.x install.
|
|
645
649
|
* **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
|
|
646
|
-
ships with OpenCode. `@opentui/core` and `solid-js` are
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
the
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
(upstream: opencode issue #33884 — `node_modules` plugins are excluded from the
|
|
654
|
-
host Solid transform and get an isolated OpenTUI copy). Because of that same
|
|
655
|
-
upstream limitation, on the **npm** path the sidebar is **auto-disabled**
|
|
656
|
-
(the plugin logs one hint and registers no slot) — use a `file://` install for
|
|
657
|
-
the live sidebar, see [Live sidebar: npm vs file:// install](#live-sidebar-npm-vs-file-install).
|
|
650
|
+
ships with OpenCode. `@opentui/core`, `@opentui/solid` and `solid-js` are
|
|
651
|
+
resolved by OpenCode at runtime from its **own** modules (the TUI loader
|
|
652
|
+
rewrites those imports to in-process instances), so the package declares them
|
|
653
|
+
as optional peers only and installs **no runtime dependencies at all**. This is
|
|
654
|
+
what makes the sidebar live for npm installs as well
|
|
655
|
+
(see [Live sidebar](#live-sidebar)); `@opentui/solid` sits in
|
|
656
|
+
`devDependencies` purely for local typechecking.
|
|
658
657
|
* **Desktop GUI part** (optional, experimental): needs an OpenCode Desktop
|
|
659
658
|
release that loads the `./gui-experimental` export (opencode 2.2.0+, where the
|
|
660
659
|
GUI plugin API exists). The bundle is prebuilt and ships in the package, so it
|
|
@@ -727,10 +726,11 @@ caller.
|
|
|
727
726
|
|
|
728
727
|
```bash
|
|
729
728
|
npm run build:gui # build dist/gui-experimental.cjs (required by the gate)
|
|
730
|
-
npm run test:prepublish # fast gate: tarball +
|
|
729
|
+
npm run test:prepublish # fast gate: tarball + JSX-free TUI + resolve + T1-T36 unit checks + GUI bundle
|
|
731
730
|
npm run test:types # tsc for the TUI entry AND the Desktop GUI part
|
|
732
|
-
npm run test:e2e # full E2E:
|
|
731
|
+
npm run test:e2e # full E2E: plugin load + one "Say OK" round-trip + npm-layout liveness
|
|
733
732
|
npm run test:e2e:quick # E2E load-only, no LLM call
|
|
733
|
+
npm run test:e2e -- --check-c # E2E npm-layout liveness only, no LLM call
|
|
734
734
|
npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
|
|
735
735
|
npm run test:drift:strict # release check: fails on ANY host drift
|
|
736
736
|
```
|
|
@@ -739,7 +739,7 @@ npm run test:drift:strict # release check: fails on ANY host drift
|
|
|
739
739
|
(`npm run build:gui && node test/prepublish.mjs && node test/e2e-tui.mjs --full`) and is skipped with
|
|
740
740
|
`--ignore-scripts`. The whole chain takes roughly **1 minute** (fast gate ~20 s,
|
|
741
741
|
isolated E2E ~10 s warm / up to ~90 s cold); the E2E is bounded by INIT 45 s +
|
|
742
|
-
STEP 80 s, ~
|
|
742
|
+
STEP 80 s + PROBE 45 s, ~4 min total.
|
|
743
743
|
|
|
744
744
|
### What the gates check
|
|
745
745
|
|
|
@@ -748,10 +748,10 @@ STEP 80 s, ~3 min total.
|
|
|
748
748
|
| Step | What | Why |
|
|
749
749
|
|---|---|---|
|
|
750
750
|
| (a) tarball composition | `package.json` `files[]` expands to the base files plus every `lib/*` module (count asserted dynamically); `docs/` may contain images only; `dist/` may contain only the built GUI bundle | no test/node_modules/tsconfig in the tarball, and no published export pointing at nothing |
|
|
751
|
-
| (b)
|
|
752
|
-
| (c) resolve chain |
|
|
751
|
+
| (b) JSX-free TUI | no `.tsx`/`.jsx` in the TUI surface (root + `lib/`), every TUI source parses with esbuild `loader:"ts"` (JSX syntax fails), no `@jsx*` pragma, imports stay inside the host-bridgeable allowlist, relative imports carry a file extension | OpenCode skips its Solid transform under `node_modules` (#33884): one JSX file freezes the npm-installed sidebar |
|
|
752
|
+
| (c) resolve chain | walks the real TUI import graph (`resolve-bridge.cjs`): every bare specifier is host-bridged, every relative one resolves to a shipped file, and **no host-bridged module is a production dependency** (a second Solid/OpenTUI instance freezes the surfaces) | an accidental runtime dependency, or a bad local import, silently breaks the sidebar |
|
|
753
753
|
| (d) unit harness T1-T36 | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache, waste-detector, compaction-reserve, top-consumer, diff, subagent-rollup and Desktop-RPC checks (86 total; T30 schema-vs-builder drift, T31 registration + handler, T32-T36 fallback-vs-hook category priority across the report, the shared state file and the live log (T36: a counter-less hook entry borrows the snapshot or prints `?`), T25c/T25d compaction-hook arming, T34 cross-instance summary merge) | regressions in modelLimits, writeStateFile, hydrate, contributors, export, subagent lifetime, and a GUI/server wire mismatch |
|
|
754
|
-
| (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.
|
|
754
|
+
| (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.ts`, `lib/*`, `gui/*`, `README.md`, `CHANGELOG.md`, `package.json`, the GUI build config) or the test tree | compliance |
|
|
755
755
|
| (f) non-English scan | no non-ASCII letters (`\p{L}` above U+007F) in the same published surface and test tree (`docs/` is image-only and is not text-scanned) | English-only source rule |
|
|
756
756
|
| (g) Desktop GUI bundle | `dist/gui-experimental.cjs` exists and is a non-trivial single-file CJS bundle: no ESM `import`, no dynamic `import()`, no Node built-in, and every app-provided module (`solid-js`, `solid-js/web`, `@opencode/plugin/gui`, `@opencode/plugin/rpc`) still a `require()` - no bundled second copy, no React/OpenTUI/bare-`opencode` import | a stale or hand-edited bundle, or a build that inlines the app instance, breaks the Desktop panel at load time |
|
|
757
757
|
|
|
@@ -761,6 +761,7 @@ STEP 80 s, ~3 min total.
|
|
|
761
761
|
|---|---|---|
|
|
762
762
|
| A — plugin load | spins a private `opencode --standalone` PTY against an **isolated global config** (`OPENCODE_CONFIG_DIR` → `%TEMP%/oci-e2e-config`, sole plugin entry = this checkout's `file://` URL), then asserts **from the host opencode log** (atomic lines, immune to PTY interleaving) that the plugin loaded and its `entrypoint` is **this checkout's `index.js`** — both fail-closed | catches a plugin that no longer imports cleanly, or an isolated config the host did not honor |
|
|
763
763
|
| B — live update | one `opencode run "Say OK" --standalone` round-trip; asserts the LLM answered **and** the isolated `state.json` gained a session with `ctx > 0` | catches a regression in the `session.idle` → `writeStateFile` path |
|
|
764
|
+
| C — npm-layout liveness | `npm pack` → extract into a throwaway `node_modules/opencode-context-indicator` → isolated `--standalone` TUI from there; an env-gated probe node (getter-driven `ref`) must produce three different rendered texts | proves the #33884 workaround: the packaged install stays **reactive** from a `node_modules` path, where the host skips the Solid transform |
|
|
764
765
|
|
|
765
766
|
The E2E is fully **non-invasive** to your running OpenCode Desktop:
|
|
766
767
|
|
|
@@ -777,6 +778,8 @@ The E2E is fully **non-invasive** to your running OpenCode Desktop:
|
|
|
777
778
|
Check B uses your real global config read-only (the round-trip needs the
|
|
778
779
|
provider/model defined there). Your real config is never modified;
|
|
779
780
|
- `OPENCODE_DISABLE_PROJECT_CONFIG=1` avoids project-level config;
|
|
781
|
+
- Check C uses its own private config, XDG roots, DB and state file under
|
|
782
|
+
`%TEMP%/oci-check-c` (nothing is shared with Check A or with your install);
|
|
780
783
|
- any `opencode-cli` instance the test itself spawned is force-killed after each
|
|
781
784
|
check (`serve --service` — your Desktop background service — is never touched);
|
|
782
785
|
- exactly **one** minimal LLM call (`Say OK`) is made, in the isolated session.
|
|
@@ -796,7 +799,7 @@ and on a daily schedule (GitHub auto-parks scheduled workflows after 60 days of
|
|
|
796
799
|
repo inactivity — a push or manual run re-enables them):
|
|
797
800
|
|
|
798
801
|
1. `npx --no-install tsc --noEmit` and `npx --no-install tsc -p tsconfig.gui.json --noEmit`
|
|
799
|
-
- typecheck of the TUI entry (`tui.
|
|
802
|
+
- typecheck of the TUI entry (`tui.ts` + its `lib/*` imports) and of the
|
|
800
803
|
Desktop GUI part (`gui/*.tsx`, Solid JSX);
|
|
801
804
|
2. `npm run build:gui` - build the Desktop GUI bundle before the gate inspects it;
|
|
802
805
|
3. `test/prepublish.mjs` - the full fast gate (a)-(g), including the standalone
|
package/index.js
CHANGED
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
* %TEMP%\opencode-context-indicator-state.json (os.tmpdir())
|
|
118
118
|
* — a machine-readable { sessionID: { model, providerID, ctx, usable,
|
|
119
119
|
* reserve, limit, categories{...}, updatedAt } } snapshot consumed by the
|
|
120
|
-
* optional TUI sidebar in ./tui.
|
|
120
|
+
* optional TUI sidebar in ./tui.ts (package entry "./tui"). Written atomically
|
|
121
121
|
* (temp + rename), keyed by session, bounded to MAX_TRACKED_SESSIONS, and
|
|
122
122
|
* strictly additive: any failure is swallowed and never affects the log.
|
|
123
123
|
*
|
|
@@ -128,9 +128,10 @@
|
|
|
128
128
|
* - Metric: the native opencode overflow count — tokens.total when present,
|
|
129
129
|
* else input + output + cache.read + cache.write — compared against the
|
|
130
130
|
* model's compaction ceiling (opencode calculateCeiling, released 2.x):
|
|
131
|
-
* window =
|
|
132
|
-
*
|
|
133
|
-
|
|
131
|
+
* window = min(input, context) when both are known, else input || context;
|
|
132
|
+
* reserve = max(15% of window, 16k when window >= 32k);
|
|
133
|
+
* usable = window - reserve. Percentages are NOT
|
|
134
|
+
* clamped at 100% - exceeding the ceiling must stay visible.
|
|
134
135
|
* - Category estimates use a unicode-aware heuristic (see estimateTokens):
|
|
135
136
|
* Cyrillic ~2.5 chars/token, CJK ~1.5, latin/ASCII ~4. These are ESTIMATES,
|
|
136
137
|
* not a tokenizer; "other" is the residual input − sum of estimates.
|
package/lib/contributors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// lib/contributors.js
|
|
2
2
|
//
|
|
3
|
-
// Pure contributor-measurement helpers extracted from
|
|
3
|
+
// Pure contributor-measurement helpers extracted from the TUI panel (lib/tui-panel.ts)
|
|
4
|
+
// for unit testing.
|
|
4
5
|
// All functions are stateless transforms of their arguments.
|
|
5
6
|
//
|
|
6
7
|
// Functions: asRecord, safeText, measureContributors, contributorLine,
|
|
@@ -185,7 +186,7 @@ function exportFileName(sessionID, now) {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
// Build the redacted export payload. All IO (mkdirSync/writeFileSync) and
|
|
188
|
-
// toasts stay in tui.
|
|
189
|
+
// toasts stay in lib/tui-panel.ts as the thin exportRedacted() wrapper.
|
|
189
190
|
//
|
|
190
191
|
// Fields: generatedAt, session{id8-prefixed id, model, providerID},
|
|
191
192
|
// family[role/model/ctx/usable/limit/pct/updated], contributors[kind/tool/ref/
|
|
@@ -193,7 +194,7 @@ function exportFileName(sessionID, now) {
|
|
|
193
194
|
// content, NO contributor preview text.
|
|
194
195
|
//
|
|
195
196
|
// panelDenom / panelRole are passed as parameters so this file stays pure and
|
|
196
|
-
// tui.
|
|
197
|
+
// lib/tui-panel.ts controls their source.
|
|
197
198
|
function buildRedactedPayload({ sessionID, root, rows, contributors, now, panelDenom, panelRole, num }) {
|
|
198
199
|
const id8 = String(sessionID).slice(0, 8);
|
|
199
200
|
return {
|
|
@@ -227,7 +228,7 @@ function buildRedactedPayload({ sessionID, root, rows, contributors, now, panelD
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
// ---------------------------------------------------------------------------
|
|
230
|
-
// Panel helpers (pure, shared with tui.
|
|
231
|
+
// Panel helpers (pure, shared with lib/tui-panel.ts)
|
|
231
232
|
// ---------------------------------------------------------------------------
|
|
232
233
|
|
|
233
234
|
// Per-row denominator with the same fallback cascade as the server table:
|
package/lib/limits.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
// Compaction ceiling (opencode calculateCeiling): opencode's core
|
|
11
11
|
// calculateCeiling() in packages/core/src/session/compaction.ts derives the
|
|
12
12
|
// auto-compaction trigger ceiling as window - reserve, where
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// window = input && context ? min(input, context) : input || context
|
|
14
|
+
// (null/zero falls through; since opencode 2.2.5 (#60660) an input limit ABOVE
|
|
15
|
+
// the context window is capped at the context window, because the context also
|
|
16
|
+
// holds the reply) and
|
|
15
17
|
// reserve = max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)
|
|
16
|
-
// `RESERVE_SHARE = 0.15` in every RELEASED host from 2.1.4 through 2.2.
|
|
18
|
+
// `RESERVE_SHARE = 0.15` in every RELEASED host from 2.1.4 through 2.2.5
|
|
17
19
|
// (verified tag by tag; the `2 * RESERVE_MIN` gate is older and unchanged). The
|
|
18
20
|
// unreleased `v2` branch HEAD has an inline `0.1` instead - do NOT mirror a
|
|
19
21
|
// branch head: a wrong reserve makes every displayed percentage wrong.
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
// plugin (the plugin cannot read opencode's internal compaction config); the
|
|
22
24
|
// env var OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER overrides the reserve for
|
|
23
25
|
// testing and power users only (non-negative finite integer <= 1e9, ignore invalid).
|
|
26
|
+
// opencode 2.2.5 also accepts a percentage there ("buffer": "20%"), which is
|
|
27
|
+
// likewise invisible here, so a percentage buffer shows the default reserve.
|
|
24
28
|
// Like core, a buffer that is NOT smaller than the window is ignored: a buffer
|
|
25
29
|
// set for a larger model must never leave zero room.
|
|
26
30
|
|
|
@@ -86,7 +90,7 @@ function getModelLimit(providerID, modelID) {
|
|
|
86
90
|
|
|
87
91
|
// Compute the reserve for a given window, matching opencode's
|
|
88
92
|
// packages/core/src/session/compaction.ts calculateCeiling() as RELEASED
|
|
89
|
-
// (RESERVE_SHARE = 0.15 in 2.1.4 .. 2.2.
|
|
93
|
+
// (RESERVE_SHARE = 0.15 in 2.1.4 .. 2.2.5; the branch head is not a release).
|
|
90
94
|
// Returns null when
|
|
91
95
|
// window is not a finite positive number.
|
|
92
96
|
const RESERVE_SHARE = 0.15; // core: RESERVE_SHARE in every released 2.x
|
|
@@ -118,7 +122,7 @@ function reserveFor(window, bufferOverride) {
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
// Native opencode compaction ceiling (calculateCeiling, released 2.x):
|
|
121
|
-
// window =
|
|
125
|
+
// window = input && context ? min(input, context) : input || context
|
|
122
126
|
// reserve = reserveFor(window) [or bufferOverride when provided]
|
|
123
127
|
// usable = window - reserve
|
|
124
128
|
// Returns { usable, limit: window, maxOutput, reserved } or null when not
|
|
@@ -127,6 +131,11 @@ function reserveFor(window, bufferOverride) {
|
|
|
127
131
|
// participate in this calculation; opencode's internal buffer is not visible.
|
|
128
132
|
// The buffer override (env or argument) replaces the whole reserve only while
|
|
129
133
|
// it is smaller than the window — a buffer >= window falls back to the share.
|
|
134
|
+
//
|
|
135
|
+
// The min() is opencode 2.2.5 (#60660): a provider that reports an input limit
|
|
136
|
+
// ABOVE the context window cannot use it in full, because the context also holds
|
|
137
|
+
// the reply. Using input alone would overstate the denominator (and show a
|
|
138
|
+
// compact-at point the host never reaches).
|
|
130
139
|
function getUsableContext(providerID, modelID, bufferOverride) {
|
|
131
140
|
const rec = getModelLimitRecord(providerID, modelID);
|
|
132
141
|
if (!rec) return null;
|
|
@@ -135,8 +144,11 @@ function getUsableContext(providerID, modelID, bufferOverride) {
|
|
|
135
144
|
const input =
|
|
136
145
|
typeof rec.input === "number" && rec.input > 0 ? rec.input : null;
|
|
137
146
|
if (input == null && context == null) return null;
|
|
138
|
-
// Mirror core's
|
|
139
|
-
|
|
147
|
+
// Mirror core's inputWindow(): min(input, context) when both are known.
|
|
148
|
+
// The `??` is core's `||` here: a non-positive input was normalized to null
|
|
149
|
+
// above, so there is no 0 to fall through.
|
|
150
|
+
const window =
|
|
151
|
+
input != null && context != null ? Math.min(input, context) : input ?? context;
|
|
140
152
|
if (window == null || !(window > 0)) return null;
|
|
141
153
|
|
|
142
154
|
// When bufferOverride is omitted, read it from the env var once per call.
|
package/lib/state.js
CHANGED
|
@@ -60,7 +60,7 @@ const LOG_FILE =
|
|
|
60
60
|
process.env.OPENCODE_CONTEXT_INDICATOR_LOG_FILE ||
|
|
61
61
|
join(tmpdir(), "context-breakdown.log");
|
|
62
62
|
|
|
63
|
-
// Machine-readable snapshot consumed by the optional TUI sidebar (./tui.
|
|
63
|
+
// Machine-readable snapshot consumed by the optional TUI sidebar (./tui.ts,
|
|
64
64
|
// package entry "./tui"). Written next to each log update — same moment, same
|
|
65
65
|
// data. See writeStateFile() below.
|
|
66
66
|
// State file path — overridable via env for test isolation.
|
|
@@ -417,7 +417,7 @@ function renameWithRetry(from, to, attempts = 3, delayMs = 50) {
|
|
|
417
417
|
// TUI state bridge (opencode-context-indicator)
|
|
418
418
|
// ---------------------------------------------------------------------------
|
|
419
419
|
// Persist a tiny machine-readable snapshot of the CURRENT breakdown for one
|
|
420
|
-
// session so the optional TUI sidebar (./tui.
|
|
420
|
+
// session so the optional TUI sidebar (./tui.ts, package entry "./tui") can
|
|
421
421
|
// render it without talking to the server. The file is one JSON object keyed by
|
|
422
422
|
// sessionID, rewritten atomically (temp + rename) at every log write — same
|
|
423
423
|
// moment, same data. Strictly additive: any failure is swallowed and must never
|
|
@@ -460,9 +460,10 @@ function writeStateFile(
|
|
|
460
460
|
try {
|
|
461
461
|
const bd = breakdownCache.get(sessionID) || null;
|
|
462
462
|
const usableInfo = getUsableContext(providerID, modelID);
|
|
463
|
-
// Persist the EFFECTIVE window (
|
|
464
|
-
// window the ceiling was derived from),
|
|
465
|
-
//
|
|
463
|
+
// Persist the EFFECTIVE window (min(input, context) when both are known,
|
|
464
|
+
// else input || context: the same window the ceiling was derived from),
|
|
465
|
+
// not the raw context window: on a cold instance
|
|
466
|
+
// hydrateModelLimitsFromState seeds context = limit, so a
|
|
466
467
|
// mismatched limit would re-derive a WRONG (too large) ceiling for models
|
|
467
468
|
// where limit.input < limit.context.
|
|
468
469
|
const limit =
|
package/lib/tui-data.d.ts
CHANGED
|
@@ -54,7 +54,6 @@ export interface StateEntry {
|
|
|
54
54
|
|
|
55
55
|
export declare const STATE_FILE: string
|
|
56
56
|
export declare const POLL_MS: number
|
|
57
|
-
export declare function isNodeModulesInstall(): boolean
|
|
58
57
|
export declare function readSnapshot(): Record<string, StateEntry>
|
|
59
58
|
export declare function collectFamily(
|
|
60
59
|
snapshot: Record<string, StateEntry>,
|