opencode-context-indicator 1.7.0 → 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 CHANGED
@@ -4,6 +4,97 @@ 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
+
64
+ ## 1.7.1 — 2026-10-29
65
+
66
+ - **Fixed: the live log no longer prints `ctx=0` above real categories.** A hook
67
+ breakdown whose instance has no request counters (a mirror that only saw the
68
+ compaction, so `input`/`ctx` are still 0) used to be printed as-is: the header
69
+ contradicted the category lines below it. Such an entry now takes the same
70
+ path as a missing/fallback one and borrows the hook-measured `state.json`
71
+ snapshot whole (categories, counters, residual and a `snapshot=` stamp; a
72
+ snapshot without a timestamp is still marked `snapshot=unknown`). A counter is
73
+ knowledge only when it is positive: a borrowed entry whose own counters are 0,
74
+ or a ctx-only snapshot with no input, prints the unknown counter as `?`, and
75
+ the residual as `n/a (input unknown on this instance)` instead of a fabricated
76
+ `0` (`other = input - sum` needs an input). The same rule now applies to a
77
+ local hook entry that knows `ctx` but not `input`. T36 covers all four cases:
78
+ whole-entry borrow, no snapshot, zero-counter snapshot, ctx-only snapshot
79
+ (86 checks).
80
+ - **Fixed: the compaction reserve is 15 % again (1.7.0 regression).** 1.7.0
81
+ switched the denominator to a 10 % share on the strength of an unreleased
82
+ branch head. Every RELEASED host from 2.1.4 through 2.2.2 (2.1.4, 2.2.0,
83
+ 2.2.1, 2.2.2) still has `RESERVE_SHARE = 0.15` in
84
+ `packages/core/src/session/compaction.ts` (verified tag by tag), so on those
85
+ hosts 1.7.0 overstated the ceiling and the `compact at` line by ~5 % of the
86
+ window (~26 k tokens at 512 k) and therefore under-reported every `%`. The
87
+ plugin now mirrors `max(floor(window * 0.15), window >= 32_000 ? 16_000 : 0)`
88
+ again; the formula, the 16 k floor and the `2 * RESERVE_MIN` gate are
89
+ unchanged. Docs, the T16/T26 tables and the seed expectations are back on the
90
+ 15 % numbers.
91
+ - Dev: the isolated e2e (plugin load + state write) was re-run against the
92
+ **2.2.2** Desktop CLI (`opencode v2.2.2`); the Desktop 2.2.2 `app.asar` still
93
+ carries the `@opencode/plugin/gui` / `@opencode/plugin/rpc` bridge and the
94
+ `ComposerPill` / `Rpc.define` / `session.panel` symbols the experimental panel
95
+ uses, and `opencode-cli.exe` still exposes `opentui:runtime-module:` plus the
96
+ `node_modules` exclusion filter.
97
+
7
98
  ## 1.7.0 — 2026-10-29
8
99
 
9
100
  Desktop GUI release (experimental): the plugin now has a surface in the
@@ -60,14 +151,14 @@ OpenCode Desktop app, not just in the terminal.
60
151
  its `ctx`/`input` counters, tagged `snapshot=<updatedAt>` — instead of this
61
152
  instance's fallback-shaped view. Repeated compactions of one session are also
62
153
  no longer collapsed into a single note when the host omits the event id.
63
- - **Reserve share corrected for opencode 2.2.x.** `calculateCeiling()` keeps the
64
- same shape but the reserve share dropped from 15 % to 10 % in 2.2.0 (the
65
- "fit output limits to the context window" change, PR #51271), so the plugin
66
- now mirrors `max(floor(window * 0.10), window >= 32_000 ? 16_000 : 0)`. Every
67
- `%` and the `compact at` line are now computed against the host's actual
68
- trigger; on a large window the old formula understated the ceiling by ~5 % of
69
- the window (~26 k tokens at 512 k). The 1.6.0 note that the ceiling was
70
- unchanged in 2.2.0 was wrong — the `2 * RESERVE_MIN` gate did stay.
154
+ - **Reserve share (claim in 1.7.0 was wrong; corrected in 1.7.1).** This
155
+ release switched the denominator to a 10 % share; no host in the verified
156
+ range 2.1.4 through 2.2.2 used it. `calculateCeiling()` in those tags keeps
157
+ `RESERVE_SHARE = 0.15`, so 1.7.0 overstated the ceiling (and the `compact at`
158
+ line) by ~5 % of the window (~26 k tokens at 512 k) on them and therefore
159
+ under-reported every `%`. The 1.6.0 note that the ceiling was unchanged in
160
+ 2.2.0 was right (the formula and the `2 * RESERVE_MIN` gate did stay); the
161
+ share line here was the wrong part.
71
162
  - Dev: `lib/format.js` (pure `fmt`/`num`) extracted so the GUI bundle has no
72
163
  `node:*` import; gate (a) now expands `dist/`, gate (e)/(f) scan `gui/` and the
73
164
  GUI build config, new gate (g) validates the built CJS bundle (app-provided
@@ -100,11 +191,13 @@ Host alignment release: opencode 2.2.0 moved TUI plugins to OpenTUI 0.7.
100
191
  opencode 2.0.16+. The TUI module now ships compiled against OpenTUI 0.7, so
101
192
  the sidebar and the `/cx` panel require **opencode ≥ 2.2.0** and cannot load on
102
193
  an older host. Older-host installation was not re-verified for this release.
103
- - **Compaction ceiling unchanged (superseded — see 1.7.0).**
194
+ - **Compaction ceiling unchanged (this note was right; 1.7.0 briefly wrong).**
104
195
  `calculateCeiling()` in 2.2.0 is identical
105
196
  to 2.1.x, so the `0.15 / 16k / 32k` reserve formula and every `%` denominator
106
197
  are unaffected. (2.2.0 changed how a *compaction request* is sized - measured
107
- prompt instead of characters - not the trigger.)
198
+ prompt instead of characters - not the trigger. 1.7.0 later claimed the share
199
+ had dropped to 10 %; see 1.7.1 - no host in the verified range
200
+ 2.1.4-2.2.2 uses it.)
108
201
  - Dev: `drift-watch` is green again (host 2.2.0, OpenTUI 0.7.0); `tsc`,
109
202
  `prepublish` and the isolated e2e pass against the 2.2.0 CLI.
110
203
 
@@ -150,8 +243,9 @@ Fix-and-hygiene release after a full-project review. No new commands.
150
243
 
151
244
  - **Compaction ceiling aligned with opencode 2.1.4.** `lib/limits.js`
152
245
  now mirrors `calculateCeiling()` exactly: the reserve is
153
- `max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)` (was 10%;
154
- the share moved again in 2.2.0 — see 1.7.0),
246
+ `max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)` (the plugin's
247
+ older builds used 10 %; no host in the verified range uses it - the share
248
+ stayed 0.15 there),
155
249
  and an override buffer is honored **only while it is smaller than the
156
250
  window** — a buffer configured for a larger model is ignored instead of
157
251
  leaving zero room. The `compact at` line and the `%` denominator now match the
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 e2e was also
19
- re-run against the 2.2.1 Desktop CLI); the V1 path against 1.18.29+.
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 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 (38%) … input=44.1k
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 (27% of input) write=1.1k (provider-reported)
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`), shown as a share of `input`. It is a *subset* of input,
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.
@@ -72,17 +75,23 @@ For every served request the plugin estimates how the context window is split:
72
75
  duplicate group), reported honestly as *"N redundant tool outputs ≈ X tokens"*
73
76
  — never as "the model wasted effort". Findings are per-session and **path-free
74
77
  in the persisted/exported form**.
75
- * The denominator is the **opencode 2.2.x compaction ceiling**: `window =
76
- limit.input || limit.context`; `reserve = max(floor(window * 0.10), window >=
78
+ * The denominator is the **opencode 2.x compaction ceiling**: `window =
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
- `calculateCeiling()` in `packages/core/src/session/compaction.ts` (the reserve
79
- share is 10% since 2.2.0 / PR #51271; it was 15% up to 2.1.x). When opencode
82
+ `calculateCeiling()` in `packages/core/src/session/compaction.ts`, whose
83
+ `RESERVE_SHARE` is 0.15 in every released host from 2.1.4 through 2.2.5 (the
84
+ unreleased `v2` branch has a different share; the plugin follows the release,
85
+ not the branch). When opencode
80
86
  carries a `compaction.buffer` configured in its own settings, the plugin
81
87
  cannot read that value (it is not exposed to plugins), so the default
82
88
  reserve formula is used — set `OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER`
83
89
  to override the reserve for testing or custom setups (non-negative integer <=
84
90
  1e9; invalid values are silently ignored; a buffer that is **not smaller than
85
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.
86
95
 
87
96
  ### Toast (V1 only)
88
97
 
@@ -114,9 +123,9 @@ other 5.6k 12%
114
123
  updated 12:30:01
115
124
  ```
116
125
 
117
- > Requires a `file://` install from a path outside `node_modules`; on the npm
118
- > path the sidebar is auto-disabled. See
119
- > [Live sidebar: npm vs file:// install](#live-sidebar-npm-vs-file-install).
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).
120
129
 
121
130
  ### Slash commands (V2)
122
131
 
@@ -125,13 +134,13 @@ commands:
125
134
 
126
135
  | Command | Scope | Delivery | Agent turn? |
127
136
  | --- | --- | --- | :---: |
128
- | `/context` | **this session only** (no subagents); cache + `state.json` only | `resume:true` — the model echoes the 3-line summary into the transcript | yes |
137
+ | `/context` | **this session only** (no subagents); cache + `state.json` only | `resume:true` - the model echoes the short summary into the transcript | yes |
129
138
  | `/context-breakdown` | this session **+ all subagent descendants** | `resume:true` — the model echoes the table into the transcript | yes |
130
139
  | `/context-diff` | **this session only**; previous measurement vs current | `resume:true` — the model echoes the delta table into the transcript | yes |
131
140
 
132
141
  #### `/context` — quick summary (main session only)
133
142
 
134
- A 3-line summary of the current session, built purely from the in-memory cache
143
+ A short summary of the current session, built purely from the in-memory cache
135
144
  and the `state.json` snapshot: **no subagent traversal and no live RPC**, so
136
145
  collection is instant (delivery itself still runs an agent turn — see below).
137
146
  The synthetic notice chip shows a self-contained one-liner (label =
@@ -155,6 +164,7 @@ instructions to follow.
155
164
  <context-summary>
156
165
  **Context** — your-model · 41.5k / 168k (25%)
157
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)
158
168
  updated 12:36:16 (snap)
159
169
  </context-summary>
160
170
  ```
@@ -264,7 +274,8 @@ the content is visible in the Desktop (the chip only shows `description`).
264
274
  `CONTEXT_BREAKDOWN_RESUME` controls `/context-breakdown` back to a plain
265
275
  notice if needed.
266
276
 
267
- > **E2E note (Desktop 2.0.19, local model `llama3.2:latest`, `build` agent).**
277
+ > **E2E note (historical run on Desktop 2.0.19, local model `llama3.2:latest`,
278
+ > `build` agent).**
268
279
  > `resume:true` behaved as designed: the synthetic item was drained into
269
280
  > `/message` and a turn ran; the assistant message contained the table verbatim
270
281
  > (the trailing `_…_` note line was dropped by the model). Because `resume:true`
@@ -360,9 +371,8 @@ carries its tool name); tool **result outputs** are counted here but not by the
360
371
  server's categories — which is why the residual `other` can look large. Top 5
361
372
  are shown. Empty state: `no messages yet`.
362
373
 
363
- Live only for a `file://` install from a path outside `node_modules`; the npm
364
- path auto-disables it (upstream `#33884`). The Desktop has no TUI, so it uses
365
- `/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.
366
376
 
367
377
  #### Export (redacted)
368
378
 
@@ -471,9 +481,8 @@ Notes — these reflect what the plugin API actually exposes today:
471
481
  therefore delivers everything through the log file, the TUI sidebar, the
472
482
  `/cx` session panel and the `/context` / `/context-breakdown` / `/context-diff` slash commands. The sidebar is a TUI-process
473
483
  slot and does not exist in the Desktop app; the slash commands work in **both**.
474
- The TUI sidebar is **live only for a `file://` install** from a path outside
475
- `node_modules`; on the npm path it is auto-disabled (upstream `#33884`) — see
476
- [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).
477
486
  * **V1 has no sidebar and no slash command.** The command/slot API (and the
478
487
  `./tui` entry) is a V2 feature; OpenCode 1.x has no equivalent, so V1 is
479
488
  toast + log only.
@@ -535,8 +544,8 @@ or add it to `opencode.jsonc`:
535
544
  ```
536
545
 
537
546
  The `./tui` sidebar entry is loaded automatically alongside the main plugin in
538
- the terminal TUI — but its live behaviour depends on the install method, see
539
- [Live sidebar: npm vs file:// install](#live-sidebar-npm-vs-file-install) below.
547
+ the terminal TUI, on every install method — see [Live sidebar](#live-sidebar)
548
+ below.
540
549
  For a CLI-only setup against remote servers, the package can also be listed in
541
550
  [`cli.json`](https://opencode.ai/v2/docs/cli/plugins):
542
551
 
@@ -544,45 +553,38 @@ For a CLI-only setup against remote servers, the package can also be listed in
544
553
  { "plugins": ["opencode-context-indicator"] }
545
554
  ```
546
555
 
547
- ### Live sidebar: npm vs file:// install
556
+ ### Live sidebar
548
557
 
549
- The TUI sidebar is a **TUI-process slot**. On OpenCode 2.x it behaves
550
- differently depending on **how the plugin was installed** — an upstream OpenCode
551
- limitation (`anomalyco/opencode#33884`), not a plugin bug: OpenCode's TUI loader
552
- skips the host Solid transform for any path inside a `node_modules` directory, so
553
- 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.
554
561
 
555
- | Install | Desktop | TUI commands | TUI live sidebar | TUI panel (/cx) |
556
- | --- | :---: | :---: | :---: | :---: |
557
- | npm (`opencode plugin add opencode-context-indicator`) | ✅ | ✅ | ❌ auto-disabled (upstream #33884) | ❌ auto-disabled |
558
- | `file://` (path **outside** any `node_modules`) | ✅ | ✅ | ✅ live | ✅ live |
559
-
560
- On the **npm** path the plugin detects that it lives under `node_modules` and
561
- **disables the sidebar and the panel gracefully** — it logs a single hint and
562
- registers no slot, so you never see a frozen "no data yet" panel. The `/context`
563
- / `/context-breakdown` / `/context-diff` slash commands and the breakdown log file keep working on
564
- every install.
565
-
566
- #### 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.
567
571
 
568
- 1. Put the package **outside** any `node_modules`: clone the repository, or
569
- unpack the npm tarball (`npm pack opencode-context-indicator`, then extract
570
- the `.tgz`) — e.g. into `C:\Users\you\plugins\opencode-context-indicator`.
571
- Double-check that the chosen path contains **no `node_modules` segment**
572
- (e.g. `C:\Users\you\plugins\...`, not `.../node_modules/...`).
573
- 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 |
574
576
 
575
- ```jsonc
576
- {
577
- "plugins": ["file:///C:/Users/you/plugins/opencode-context-indicator"]
578
- }
579
- ```
577
+ `/context`, `/context-breakdown`, `/context-diff` and the breakdown log file work
578
+ on every install regardless.
580
579
 
581
- 3. Restart OpenCode. The sidebar is now live and updates as context grows.
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.
582
583
 
583
- > ⚠️ The `file://` path must **not** contain a `node_modules` segment. If it
584
- > does, OpenCode skips the Solid transform and the sidebar will not update; the
585
- > plugin auto-disables it and logs the hint above.
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).
586
588
 
587
589
  ### OpenCode 1.x (V1)
588
590
 
@@ -599,25 +601,28 @@ Add the package name to the `plugin` array in your `opencode.json`:
599
601
  Copy `index.js` and the whole `lib/` directory into
600
602
  `~/.config/opencode/plugins/`, keeping the `lib/` subdirectory next to the
601
603
  plugin file so the plugin's relative imports resolve. The `lib/` folder is
602
- 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:
603
611
 
604
612
  ```
605
- ~/.config/opencode/plugins/context-indicator.js
606
- ~/.config/opencode/plugins/lib/cache.js
607
- ~/.config/opencode/plugins/lib/commands.js
608
- ~/.config/opencode/plugins/lib/dedup.js
609
- ~/.config/opencode/plugins/lib/estimate.js
610
- ~/.config/opencode/plugins/lib/limits.js
611
- ~/.config/opencode/plugins/lib/state.js
612
- ~/.config/opencode/plugins/lib/v1.js
613
- ~/.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
614
616
  ```
615
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
+
616
623
  To use the sidebar locally as well, point OpenCode at a `file://` copy of the
617
- package that lives **outside any `node_modules`** (see
618
- [Live sidebar: npm vs file:// install](#live-sidebar-npm-vs-file-install)) — that
619
- is the only install path that keeps the sidebar live. An npm install auto-disables
620
- the sidebar instead.
624
+ package — the sidebar is live there too, exactly as with an npm install (see
625
+ [Live sidebar](#live-sidebar)).
621
626
 
622
627
  ---
623
628
 
@@ -633,25 +638,22 @@ the sidebar instead.
633
638
  indicator degrades
634
639
  gracefully (the breakdown falls back to throttled `session.messages` fetches
635
640
  and toasts keep working).
636
- * **OpenCode ≥ 2.2.0** for the V2 path with the TUI surfaces (built and verified
637
- against **2.2.0**; the isolated load + state-write e2e also passes on the
638
- **2.2.1** Desktop CLI). The server-only V2 path keeps working on
639
- 2.0.16+; the TUI module pins the OpenTUI generation it was compiled against,
640
- so a host on OpenTUI 0.6 (opencode ≤ 2.1.4) will not load the TUI surfaces -
641
- neither the sidebar nor the `/cx` panel - from a 1.6.x install.
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.
642
649
  * **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
643
- ships with OpenCode. `@opentui/core` and `solid-js` are **optional** peer
644
- dependencies resolved by OpenCode at runtime; the main plugin installs and runs
645
- fine without them (the sidebar simply is not available). `@opentui/solid` is
646
- instead shipped as a **pinned direct dependency** (exact `0.7.0`, matching
647
- the OpenTUI stack OpenCode 2.2.0 ships): OpenCode's
648
- TUI loader does not expose a host instance of it, so for npm-installed plugins
649
- the JSX pragma would otherwise fail to resolve `@opentui/solid/jsx-runtime`
650
- (upstream: opencode issue #33884 — `node_modules` plugins are excluded from the
651
- host Solid transform and get an isolated OpenTUI copy). Because of that same
652
- upstream limitation, on the **npm** path the sidebar is **auto-disabled**
653
- (the plugin logs one hint and registers no slot) — use a `file://` install for
654
- 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.
655
657
  * **Desktop GUI part** (optional, experimental): needs an OpenCode Desktop
656
658
  release that loads the `./gui-experimental` export (opencode 2.2.0+, where the
657
659
  GUI plugin API exists). The bundle is prebuilt and ships in the package, so it
@@ -724,10 +726,11 @@ caller.
724
726
 
725
727
  ```bash
726
728
  npm run build:gui # build dist/gui-experimental.cjs (required by the gate)
727
- npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T35 unit checks + GUI bundle
729
+ npm run test:prepublish # fast gate: tarball + JSX-free TUI + resolve + T1-T36 unit checks + GUI bundle
728
730
  npm run test:types # tsc for the TUI entry AND the Desktop GUI part
729
- npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
731
+ npm run test:e2e # full E2E: plugin load + one "Say OK" round-trip + npm-layout liveness
730
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
731
734
  npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
732
735
  npm run test:drift:strict # release check: fails on ANY host drift
733
736
  ```
@@ -736,7 +739,7 @@ npm run test:drift:strict # release check: fails on ANY host drift
736
739
  (`npm run build:gui && node test/prepublish.mjs && node test/e2e-tui.mjs --full`) and is skipped with
737
740
  `--ignore-scripts`. The whole chain takes roughly **1 minute** (fast gate ~20 s,
738
741
  isolated E2E ~10 s warm / up to ~90 s cold); the E2E is bounded by INIT 45 s +
739
- STEP 80 s, ~3 min total.
742
+ STEP 80 s + PROBE 45 s, ~4 min total.
740
743
 
741
744
  ### What the gates check
742
745
 
@@ -745,10 +748,10 @@ STEP 80 s, ~3 min total.
745
748
  | Step | What | Why |
746
749
  |---|---|---|
747
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 |
748
- | (b) pragma regression | esbuild `--jsx=automatic` → `@opentui/solid/jsx-runtime` present, `react/jsx-runtime` absent | a removed/broken pragma kills the sidebar on npm install |
749
- | (c) resolve chain | replica install: host-bridged + peer packages resolve; local tui.tsx relative imports resolve via node from the entry (resolve-bridge.cjs) | broken peer/optional dep or a bad local import silently breaks the sidebar |
750
- | (d) unit harness T1-T35 | 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 (85 total; T30 schema-vs-builder drift, T31 registration + handler, T32-T35 fallback-vs-hook category priority across the report, the shared state file and the live log, 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 |
751
- | (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.tsx`, `lib/*`, `gui/*`, `README.md`, `CHANGELOG.md`, `package.json`, the GUI build config) or the test tree | compliance |
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
+ | (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.ts`, `lib/*`, `gui/*`, `README.md`, `CHANGELOG.md`, `package.json`, the GUI build config) or the test tree | compliance |
752
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 |
753
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 |
754
757
 
@@ -758,6 +761,7 @@ STEP 80 s, ~3 min total.
758
761
  |---|---|---|
759
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 |
760
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 |
761
765
 
762
766
  The E2E is fully **non-invasive** to your running OpenCode Desktop:
763
767
 
@@ -774,6 +778,8 @@ The E2E is fully **non-invasive** to your running OpenCode Desktop:
774
778
  Check B uses your real global config read-only (the round-trip needs the
775
779
  provider/model defined there). Your real config is never modified;
776
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);
777
783
  - any `opencode-cli` instance the test itself spawned is force-killed after each
778
784
  check (`serve --service` — your Desktop background service — is never touched);
779
785
  - exactly **one** minimal LLM call (`Say OK`) is made, in the isolated session.
@@ -793,7 +799,7 @@ and on a daily schedule (GitHub auto-parks scheduled workflows after 60 days of
793
799
  repo inactivity — a push or manual run re-enables them):
794
800
 
795
801
  1. `npx --no-install tsc --noEmit` and `npx --no-install tsc -p tsconfig.gui.json --noEmit`
796
- - typecheck of the TUI entry (`tui.tsx` + its `lib/*` imports) and of the
802
+ - typecheck of the TUI entry (`tui.ts` + its `lib/*` imports) and of the
797
803
  Desktop GUI part (`gui/*.tsx`, Solid JSX);
798
804
  2. `npm run build:gui` - build the Desktop GUI bundle before the gate inspects it;
799
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.tsx (package entry "./tui"). Written atomically
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
  *
@@ -127,11 +127,11 @@
127
127
  *
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
- * model's compaction ceiling (opencode calculateCeiling, core 2.2.x):
131
- * window = limit.input || limit.context, reserve = max(10% of window, 16k
132
- * when window >= 32k), usable = window - reserve (the share was 15% up to
133
- * 2.1.x). Percentages are NOT
134
- * clamped at 100% - exceeding the ceiling must stay visible.
130
+ * model's compaction ceiling (opencode calculateCeiling, released 2.x):
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.
135
135
  * - Category estimates use a unicode-aware heuristic (see estimateTokens):
136
136
  * Cyrillic ~2.5 chars/token, CJK ~1.5, latin/ASCII ~4. These are ESTIMATES,
137
137
  * not a tokenizer; "other" is the residual input − sum of estimates.
@@ -1,6 +1,7 @@
1
1
  // lib/contributors.js
2
2
  //
3
- // Pure contributor-measurement helpers extracted from tui.tsx for unit testing.
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.tsx as the thin exportRedacted() wrapper.
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.tsx controls their source.
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.tsx)
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: