opencode-context-indicator 1.5.2 → 1.5.4
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 +72 -0
- package/README.md +91 -40
- package/docs/sidebar.png +0 -0
- package/index.js +22 -6
- package/lib/cache.js +37 -3
- package/lib/commands.js +170 -0
- package/lib/estimate.js +84 -0
- package/lib/limits.js +20 -11
- package/lib/state.js +100 -19
- package/lib/tui-panel.tsx +95 -3
- package/lib/v2.js +94 -13
- package/package.json +6 -5
- package/tui.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,78 @@ 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.5.4 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
Fix-and-hygiene release after a full-project review. No new commands.
|
|
10
|
+
|
|
11
|
+
- **Subagent roll-up no longer collapses on a child's idle.** `session.idle`
|
|
12
|
+
wiped the child's parent attribution and accumulated totals, so the very next
|
|
13
|
+
live line reported `subagents 0 session(s)` right after any subagent
|
|
14
|
+
finished, even though its parent was still running. V2 now keeps a finished
|
|
15
|
+
child's entries until the parent itself goes idle or is deleted
|
|
16
|
+
(`clearChildSubagentTracking`, breadth-first, so sub-subagents are collected
|
|
17
|
+
too); `/context-breakdown` and `/cx` were never affected (they read
|
|
18
|
+
`state.json`). T29 covers the lifetime.
|
|
19
|
+
- **TUI panel no longer re-measures every message once per second.** The
|
|
20
|
+
measured-contributors memo was keyed on the 1000 ms snapshot tick and the host
|
|
21
|
+
rebuilds the message array on every poll, so the full walk (per-character
|
|
22
|
+
token estimates plus `JSON.stringify` of tool args) ran on the render thread
|
|
23
|
+
every second. It is now keyed on a cheap signature (list length, last message
|
|
24
|
+
id, last message size including tool results and status), so a static session
|
|
25
|
+
costs almost nothing while streaming text and completing tool calls still
|
|
26
|
+
refresh.
|
|
27
|
+
- **A summary-only log write keeps the live snapshot.** `pushFinalSummary` used
|
|
28
|
+
to rewrite `context-breakdown.log` with an empty live section, so every idle /
|
|
29
|
+
compaction / command payload blanked it until the next measured write. The
|
|
30
|
+
module now remembers the last rendered snapshot and re-renders it.
|
|
31
|
+
- Doc fixes: the log is rewritten atomically rather than "appended" with
|
|
32
|
+
`appendFileSync`, and both its sections are process-local (survive rewrites,
|
|
33
|
+
not a restart); the `state.json` field list now includes `toolOutByTool`,
|
|
34
|
+
`reclaim`, `cacheRead`/`cacheWrite` and `compactedAt`/`compactStale`; "tested
|
|
35
|
+
against opencode" now says 2.1.4 (was a stale v2.0.28); the gate table now
|
|
36
|
+
states the actual scan scope.
|
|
37
|
+
- Packaging: `docs/` is published, so the README screenshot resolves on the npm
|
|
38
|
+
page. Gate (a) expands `docs/` the same way it expands `lib/` and rejects any
|
|
39
|
+
non-image file there.
|
|
40
|
+
- Tests: new `OPENCODE_CONTEXT_INDICATOR_LOG_FILE` override (test isolation -
|
|
41
|
+
idle/compacted handlers no longer rewrite the user's real
|
|
42
|
+
`context-breakdown.log`), T29 added (child / grand-child lifetime). 75 unit
|
|
43
|
+
checks.
|
|
44
|
+
|
|
45
|
+
## 1.5.3 — 2026-10-26
|
|
46
|
+
|
|
47
|
+
- **Compaction ceiling aligned with opencode 2.1.4 (#51271).** `lib/limits.js`
|
|
48
|
+
now mirrors `calculateCeiling()` exactly: the reserve is
|
|
49
|
+
`max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)` (was 10%),
|
|
50
|
+
and an override buffer is honored **only while it is smaller than the
|
|
51
|
+
window** — a buffer configured for a larger model is ignored instead of
|
|
52
|
+
leaving zero room. The `compact at` line and the `%` denominator now match the
|
|
53
|
+
host's actual auto-compaction trigger.
|
|
54
|
+
- **Top context consumers.** `/context-breakdown` adds a
|
|
55
|
+
`### Top context consumers (main)` section listing the 5 heaviest single items
|
|
56
|
+
still in the assembled request (one tool result, one message, the system
|
|
57
|
+
prompt, one tool-call's args) with their share of `input`. Tool targets are
|
|
58
|
+
shown live; the `state.json` copy (`top`) is path-free, like the waste
|
|
59
|
+
detector.
|
|
60
|
+
- **`/context-diff`.** New slash command: the previous distinct measurement vs
|
|
61
|
+
the current one (ctx total + per-category deltas, unchanged rows omitted).
|
|
62
|
+
`state.json` persists a `previous` snapshot; an unchanged / mirror write
|
|
63
|
+
inherits it, so the diff always spans two different observations. Main
|
|
64
|
+
session only, cache + `state.json` only (no RPC), delivered under the
|
|
65
|
+
`<context-diff>` tag with `resume:true` like the other commands.
|
|
66
|
+
- Dev: `@opencode/plugin` devDependency pinned to `2.1.4`; `@opentui/solid`
|
|
67
|
+
and `@opentui/core` aligned with the 2.1.4 host (`0.6.8`). The 2.1.4 type
|
|
68
|
+
changes are internal (`DeepMutable` re-exported from effect; an unused
|
|
69
|
+
`permission.descending` brand); the `./tui` surface we import is unchanged.
|
|
70
|
+
`@opentui/solid` 0.6.8 is byte-identical to 0.6.6 in `dist/` and
|
|
71
|
+
`scripts/`; only its `@opentui/core` dependency moved to 0.6.8.
|
|
72
|
+
- Tests: T26 (2.1.4 reserve table + buffer-over-window guard), T27/T27a/T27b
|
|
73
|
+
(top consumers: estimate → path-free persistence → rendered section),
|
|
74
|
+
T28/T28a/T28b/T28c/T28d/T28e (`previous` inheritance → diff rendering →
|
|
75
|
+
honest empty state → unmeasured-write guard (known + unknown model) →
|
|
76
|
+
null-safe delta); existing
|
|
77
|
+
T2/T4/T4b/T7/T9/T10/T12/T16* updated for the 0.15 reserve. 74 unit checks.
|
|
78
|
+
|
|
7
79
|
## 1.5.2 — 2026-10-21
|
|
8
80
|
|
|
9
81
|
Maintenance release: no runtime behavior change. It aligns the dev toolchain
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ arguments, tool results (the outputs fed back into context) and the residual
|
|
|
13
13
|
The package ships a **dual V1 + V2 plugin** (one entry works in both OpenCode
|
|
14
14
|
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
|
-
and a redacted export. The V2 path is **tested against opencode
|
|
16
|
+
and a redacted export. The V2 path is **tested against opencode 2.1.4**; the
|
|
17
17
|
V1 path against 1.18.29+.
|
|
18
18
|
|
|
19
19
|
> **V1 is feature-frozen.** It is kept working for compatibility (bug fixes
|
|
@@ -70,14 +70,16 @@ For every served request the plugin estimates how the context window is split:
|
|
|
70
70
|
duplicate group), reported honestly as *"N redundant tool outputs ≈ X tokens"*
|
|
71
71
|
— never as "the model wasted effort". Findings are per-session and **path-free
|
|
72
72
|
in the persisted/exported form**.
|
|
73
|
-
* The denominator is the **opencode
|
|
74
|
-
limit.input || limit.context`; `reserve = max(floor(window * 0.
|
|
75
|
-
|
|
73
|
+
* The denominator is the **opencode 2.1.4 compaction ceiling**: `window =
|
|
74
|
+
limit.input || limit.context`; `reserve = max(floor(window * 0.15), window >=
|
|
75
|
+
2 * 16_000 ? 16_000 : 0)`; the ceiling is `window - reserve`. This mirrors
|
|
76
|
+
`calculateCeiling()` in `packages/core/src/session/compaction.ts`. When opencode
|
|
76
77
|
carries a `compaction.buffer` configured in its own settings, the plugin
|
|
77
78
|
cannot read that value (it is not exposed to plugins), so the default
|
|
78
79
|
reserve formula is used — set `OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER`
|
|
79
80
|
to override the reserve for testing or custom setups (non-negative integer <=
|
|
80
|
-
1e9; invalid values are silently ignored
|
|
81
|
+
1e9; invalid values are silently ignored; a buffer that is **not smaller than
|
|
82
|
+
the window** is ignored, exactly like core, so it can never leave zero room).
|
|
81
83
|
|
|
82
84
|
### Toast (V1 only)
|
|
83
85
|
|
|
@@ -115,13 +117,14 @@ updated 12:30:01
|
|
|
115
117
|
|
|
116
118
|
### Slash commands (V2)
|
|
117
119
|
|
|
118
|
-
On OpenCode 2.x (Desktop **and** terminal TUI) the plugin registers
|
|
120
|
+
On OpenCode 2.x (Desktop **and** terminal TUI) the plugin registers three slash
|
|
119
121
|
commands:
|
|
120
122
|
|
|
121
123
|
| Command | Scope | Delivery | Agent turn? |
|
|
122
124
|
| --- | --- | --- | :---: |
|
|
123
125
|
| `/context` | **this session only** (no subagents); cache + `state.json` only | `resume:true` — the model echoes the 3-line summary into the transcript | yes |
|
|
124
126
|
| `/context-breakdown` | this session **+ all subagent descendants** | `resume:true` — the model echoes the table into the transcript | yes |
|
|
127
|
+
| `/context-diff` | **this session only**; previous measurement vs current | `resume:true` — the model echoes the delta table into the transcript | yes |
|
|
125
128
|
|
|
126
129
|
#### `/context` — quick summary (main session only)
|
|
127
130
|
|
|
@@ -179,6 +182,14 @@ unavailable._
|
|
|
179
182
|
### Tool results by tool (main)
|
|
180
183
|
webfetch 242.1k · read 18.4k · bash 4.9k · +3 more
|
|
181
184
|
|
|
185
|
+
### Top context consumers (main)
|
|
186
|
+
1. tool result · webfetch https://example.com/large-page — ~242.1k (61% of input)
|
|
187
|
+
2. tool result · read src/a.ts — ~18.4k (5% of input)
|
|
188
|
+
3. system prompt — ~8.5k (2% of input)
|
|
189
|
+
4. reasoning — ~4.5k (1% of input)
|
|
190
|
+
5. tool args · bash npm test — ~1.2k (0% of input)
|
|
191
|
+
_Heaviest single items still in the request; one item per line, tokens are estimates._
|
|
192
|
+
|
|
182
193
|
### Prompt cache (main, provider-reported)
|
|
183
194
|
read 12.0k (27% of input) · write 1.1k
|
|
184
195
|
|
|
@@ -203,6 +214,34 @@ instructions to follow.
|
|
|
203
214
|
</context-breakdown>
|
|
204
215
|
```
|
|
205
216
|
|
|
217
|
+
The top-5 heaviest **single items** still in the assembled request (one tool
|
|
218
|
+
result, one message, the system prompt) are listed separately, with their share
|
|
219
|
+
of `input`. Tool targets (file paths / URLs) are shown in the live section only;
|
|
220
|
+
the `state.json` copy is path-free, like the waste detector.
|
|
221
|
+
|
|
222
|
+
#### `/context-diff` — what changed since the previous measurement
|
|
223
|
+
|
|
224
|
+
`state.json` keeps one `previous` measurement per session: the record that was
|
|
225
|
+
**replaced** by the most recent distinct write. `/context-diff` compares it with
|
|
226
|
+
the current one and renders only the rows that actually moved:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
**Context diff** — your-model
|
|
230
|
+
total 41.5k -> 45.2k (+3.7k tokens, +9%)
|
|
231
|
+
|
|
232
|
+
| category | previous | now | delta |
|
|
233
|
+
| --- | --- | --- | --- |
|
|
234
|
+
| toolOut | 24.7k | 28.4k | +3.7k |
|
|
235
|
+
|
|
236
|
+
_previous 12:36:16 · current 12:40:11 · tokens are estimates._
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Collection is instant (cache + `state.json`, no subagent traversal, no RPC) and
|
|
240
|
+
delivery uses the same `resume:true` + `<context-diff>` tag protocol as the other
|
|
241
|
+
commands. An unchanged (mirror / repeated) write does **not** shift `previous`,
|
|
242
|
+
so the diff always spans two different observations. A session with only one
|
|
243
|
+
measurement reports `No previous measurement yet`.
|
|
244
|
+
|
|
206
245
|
#### Delivery semantics (`resume`)
|
|
207
246
|
|
|
208
247
|
`ctx.session.synthetic({ sessionID, text, description, resume })` durably *admits*
|
|
@@ -215,8 +254,8 @@ session is woken (opencode `Session.synthetic`:
|
|
|
215
254
|
| `false` | durable inbox notice; **no** LLM request; session context unchanged. A synthetic message is rendered as a compact **Notice chip** whose label is `description ?? text` (the client row-builder maps `synthetic` → Notice) — so the chip shows the description, **not** the table. |
|
|
216
255
|
| `true` | the same inbox item **plus** a wake → a real agent turn runs. The model sees the payload as input and its answer is a **normal assistant message**, rendered in full. |
|
|
217
256
|
|
|
218
|
-
|
|
219
|
-
(`<context-summary>` / `<context-breakdown>`). A real agent turn runs and the
|
|
257
|
+
All three commands use `resume:true` with the payload wrapped in explicit tags
|
|
258
|
+
(`<context-summary>` / `<context-breakdown>` / `<context-diff>`). A real agent turn runs and the
|
|
220
259
|
model renders the tagged content as a normal assistant message — the only way
|
|
221
260
|
the content is visible in the Desktop (the chip only shows `description`).
|
|
222
261
|
`CONTEXT_BREAKDOWN_RESUME` controls `/context-breakdown` back to a plain
|
|
@@ -231,10 +270,10 @@ notice if needed.
|
|
|
231
270
|
> exist). Treat `/context-breakdown` as *"ask the agent to show the table"*, not
|
|
232
271
|
> as a pure UI action.
|
|
233
272
|
|
|
234
|
-
* **
|
|
235
|
-
cache and `state.json` only (no RPC, no subagent
|
|
236
|
-
`/context-breakdown` collects the full tree, but
|
|
237
|
-
`resume:true` — so each issues a real LLM request and the session context
|
|
273
|
+
* **All three commands run an agent turn.** `/context` and `/context-diff`
|
|
274
|
+
collect from the in-memory cache and `state.json` only (no RPC, no subagent
|
|
275
|
+
traversal) and `/context-breakdown` collects the full tree, but all deliver
|
|
276
|
+
with `resume:true` — so each issues a real LLM request and the session context
|
|
238
277
|
grows by that turn.
|
|
239
278
|
* Subagents are discovered by walking the **parentID chain persisted in
|
|
240
279
|
`state.json`** (breadth-first from the current session, every level — subagents
|
|
@@ -263,15 +302,16 @@ notice if needed.
|
|
|
263
302
|
are also seeded from the snapshot at startup, and the retry backs off
|
|
264
303
|
(30 s → 5 min, capped). A known `usable`/`limit`/`agent` is never overwritten
|
|
265
304
|
with `null` when another instance rewrites the snapshot.
|
|
266
|
-
*
|
|
267
|
-
section), so it stays recoverable even if
|
|
305
|
+
* All three commands copy their payload into `context-breakdown.log` (final-summaries
|
|
306
|
+
section) while the plugin instance lives, so it stays recoverable even if
|
|
307
|
+
delivery fails (see the log lifetime note under "Log / state file locations").
|
|
268
308
|
* If `ctx.session.synthetic` throws, the command logs an actionable error and the
|
|
269
309
|
payload is preserved in the log — there is no other delivery channel in this
|
|
270
310
|
runtime (no `noReply`/`prompt` option exists).
|
|
271
311
|
* **Instruction echo.** Delivery is an agent turn: the model renders the
|
|
272
312
|
payload, and with the previous instruction wording it occasionally echoed
|
|
273
|
-
the instruction line itself.
|
|
274
|
-
explicit tags (`<context-summary>` / `<context-breakdown>`) and instruct the
|
|
313
|
+
the instruction line itself. All instructions now delimit the payload with
|
|
314
|
+
explicit tags (`<context-summary>` / `<context-breakdown>` / `<context-diff>`) and instruct the
|
|
275
315
|
model to output only the tagged content — an unambiguous payload boundary
|
|
276
316
|
instead of a "reproduce exactly" phrasing. Residual risk: a model may still
|
|
277
317
|
add light commentary or include the tags themselves.
|
|
@@ -304,8 +344,8 @@ session view (the palette itself is only reachable outside the composer).
|
|
|
304
344
|
Opened from a subagent view, the family is rooted at the topmost ancestor, so
|
|
305
345
|
the whole family is shown, not just the child subtree.
|
|
306
346
|
|
|
307
|
-
**Zero LLM calls.** Unlike
|
|
308
|
-
|
|
347
|
+
**Zero LLM calls.** Unlike the slash commands (which run a real agent turn so
|
|
348
|
+
the model renders the output), the panel only reads local TUI data
|
|
309
349
|
and the state file; it never wakes the session and never grows the context.
|
|
310
350
|
|
|
311
351
|
The **largest contributors (measured)** section char-counts the in-process
|
|
@@ -319,7 +359,7 @@ are shown. Empty state: `no messages yet`.
|
|
|
319
359
|
|
|
320
360
|
Live only for a `file://` install from a path outside `node_modules`; the npm
|
|
321
361
|
path auto-disables it (upstream `#33884`). The Desktop has no TUI, so it uses
|
|
322
|
-
`/context` and `/context-
|
|
362
|
+
`/context`, `/context-breakdown` and `/context-diff` instead.
|
|
323
363
|
|
|
324
364
|
#### Export (redacted)
|
|
325
365
|
|
|
@@ -345,7 +385,7 @@ Notes — these reflect what the plugin API actually exposes today:
|
|
|
345
385
|
* **V2 has no toast channel on the server side.** The V2 plugin API
|
|
346
386
|
(`ctx.*`) exposes no TUI/toast method (opencode issue `#49380`); the V2 path
|
|
347
387
|
therefore delivers everything through the log file, the TUI sidebar, the
|
|
348
|
-
`/cx` session panel and the `/context` / `/context-breakdown` slash commands. The sidebar is a TUI-process
|
|
388
|
+
`/cx` session panel and the `/context` / `/context-breakdown` / `/context-diff` slash commands. The sidebar is a TUI-process
|
|
349
389
|
slot and does not exist in the Desktop app; the slash commands work in **both**.
|
|
350
390
|
The TUI sidebar is **live only for a `file://` install** from a path outside
|
|
351
391
|
`node_modules`; on the npm path it is auto-disabled (upstream `#33884`) — see
|
|
@@ -354,7 +394,7 @@ Notes — these reflect what the plugin API actually exposes today:
|
|
|
354
394
|
`./tui` entry) is a V2 feature; OpenCode 1.x has no equivalent, so V1 is
|
|
355
395
|
toast + log only.
|
|
356
396
|
* The Desktop app is TUI-less; its V2 surfaces are the log file and the
|
|
357
|
-
`/context` / `/context-breakdown` slash commands.
|
|
397
|
+
`/context` / `/context-breakdown` / `/context-diff` slash commands.
|
|
358
398
|
|
|
359
399
|
---
|
|
360
400
|
|
|
@@ -366,16 +406,24 @@ and `$TMPDIR` (usually `/tmp`) elsewhere:
|
|
|
366
406
|
| File | Purpose |
|
|
367
407
|
| --- | --- |
|
|
368
408
|
| `context-breakdown.log` | Human-readable live snapshot + bounded final summaries |
|
|
369
|
-
| `opencode-context-indicator-state.json` | Machine-readable snapshot consumed by the TUI sidebar and the `/cx` panel. Fields per entry: `sessionID`, `parentID`, `role`, `agent`, `model`, `providerID`, `ctx`, `input`, `usable` (compaction ceiling), `reserve` (tokens reserved for compaction), `limit` (context window), `reasoning`, `categories{user,assistant,reasoning,toolArgs,system,toolSchemas,other}`, `updatedAt`. |
|
|
409
|
+
| `opencode-context-indicator-state.json` | Machine-readable snapshot consumed by the TUI sidebar and the `/cx` panel. Fields per entry: `sessionID`, `parentID`, `role`, `agent`, `model`, `providerID`, `ctx`, `input`, `usable` (compaction ceiling), `reserve` (tokens reserved for compaction), `limit` (context window), `reasoning`, `categories{user,assistant,reasoning,toolArgs,toolOut,system,toolSchemas,other}`, `toolOutByTool` (per-tool repeated-output estimate), `reclaim` (duplicate tool output, bounded), `cacheRead`/`cacheWrite` (provider-reported prompt-cache tokens), `compactedAt`/`compactStale`, `top` (top-5 heaviest single items, path-free), `previous` (previous distinct measurement for `/context-diff`), `updatedAt`. |
|
|
370
410
|
| `context-events.log` | Raw event tap — **only** when `DEBUG_EVENTS` is flipped to `true` in the source (off by default) |
|
|
371
411
|
|
|
372
|
-
The
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
412
|
+
The whole `context-breakdown.log` is **rewritten atomically** (temp file +
|
|
413
|
+
rename, like the state file): the live snapshot is always regenerated and the
|
|
414
|
+
final summaries (on `session.idle`) plus compaction notes are re-rendered from
|
|
415
|
+
an in-memory buffer (bounded, last 50 per log), so the file never grows and
|
|
416
|
+
concurrent plugin instances cannot observe a half-written file. A summary-only
|
|
417
|
+
write (idle, compaction, command payload) re-renders the last live snapshot, so
|
|
418
|
+
adding a note never blanks that section. That snapshot may predate a compaction
|
|
419
|
+
or a deleted session - the leading `[<timestamp>]` line is the freshness cue.
|
|
420
|
+
Both parts are **process-local**: they
|
|
421
|
+
survive every rewrite and failed command delivery within the lifetime of the
|
|
422
|
+
running instance, but they are dropped when the plugin instance is re-created
|
|
423
|
+
(opencode restarts, or about 24 h of idling). A different instance that rewrites
|
|
424
|
+
the file keeps its own view of the summary buffer; cross-instance duplicate
|
|
425
|
+
summaries are prevented separately by an atomic claim marker (see
|
|
426
|
+
`lib/dedup.js`).
|
|
379
427
|
|
|
380
428
|
---
|
|
381
429
|
|
|
@@ -425,7 +473,7 @@ a slot mounted from an npm install renders once and then never live-updates.
|
|
|
425
473
|
On the **npm** path the plugin detects that it lives under `node_modules` and
|
|
426
474
|
**disables the sidebar and the panel gracefully** — it logs a single hint and
|
|
427
475
|
registers no slot, so you never see a frozen "no data yet" panel. The `/context`
|
|
428
|
-
/ `/context-breakdown` slash commands and the breakdown log file keep working on
|
|
476
|
+
/ `/context-breakdown` / `/context-diff` slash commands and the breakdown log file keep working on
|
|
429
477
|
every install.
|
|
430
478
|
|
|
431
479
|
#### Getting the live sidebar (`file://` install)
|
|
@@ -499,13 +547,13 @@ the sidebar instead.
|
|
|
499
547
|
gracefully (the breakdown falls back to throttled `session.messages` fetches
|
|
500
548
|
and toasts keep working).
|
|
501
549
|
* **OpenCode ≥ 2.0.16** for the V2 path (built and verified against
|
|
502
|
-
**2.1.
|
|
550
|
+
**2.1.4**).
|
|
503
551
|
* **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
|
|
504
552
|
ships with OpenCode. `@opentui/core` and `solid-js` are **optional** peer
|
|
505
553
|
dependencies resolved by OpenCode at runtime; the main plugin installs and runs
|
|
506
554
|
fine without them (the sidebar simply is not available). `@opentui/solid` is
|
|
507
|
-
instead shipped as a **pinned direct dependency** (exact `0.6.
|
|
508
|
-
the OpenTUI stack OpenCode 2.1.
|
|
555
|
+
instead shipped as a **pinned direct dependency** (exact `0.6.8`, matching
|
|
556
|
+
the OpenTUI stack OpenCode 2.1.4 ships): OpenCode's
|
|
509
557
|
TUI loader does not expose a host instance of it, so for npm-installed plugins
|
|
510
558
|
the JSX pragma would otherwise fail to resolve `@opentui/solid/jsx-runtime`
|
|
511
559
|
(upstream: opencode issue #33884 — `node_modules` plugins are excluded from the
|
|
@@ -535,7 +583,7 @@ The package default export is a dual plugin:
|
|
|
535
583
|
request), subscribes to the event stream (`session.created`,
|
|
536
584
|
`session.step.ended`, `session.idle`, `session.compacted`, failure events),
|
|
537
585
|
records limits from the model registry and registers the
|
|
538
|
-
`/context` + `/context-breakdown` slash commands (via
|
|
586
|
+
`/context` + `/context-breakdown` + `/context-diff` slash commands (via
|
|
539
587
|
`ctx.command.transform`). Read-only: the context is never mutated (the
|
|
540
588
|
commands wake the session via `resume:true`, which is what renders the table).
|
|
541
589
|
* `server({ client })` — V1 entry (OpenCode ≥ 1.18.29). Classic event handler +
|
|
@@ -578,7 +626,7 @@ caller.
|
|
|
578
626
|
### Running tests
|
|
579
627
|
|
|
580
628
|
```bash
|
|
581
|
-
npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-
|
|
629
|
+
npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T29 unit checks
|
|
582
630
|
npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
|
|
583
631
|
npm run test:e2e:quick # E2E load-only, no LLM call
|
|
584
632
|
npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
|
|
@@ -597,12 +645,12 @@ STEP 80 s, ~3 min total.
|
|
|
597
645
|
|
|
598
646
|
| Step | What | Why |
|
|
599
647
|
|---|---|---|
|
|
600
|
-
| (a) tarball composition | `package.json` `files[]` expands to the base files plus every `lib/*` module (count asserted dynamically) | no test/node_modules/tsconfig in the tarball |
|
|
648
|
+
| (a) tarball composition | `package.json` `files[]` expands to the base files plus every `lib/*` module (count asserted dynamically); `docs/` may contain images only | no test/node_modules/tsconfig in the tarball |
|
|
601
649
|
| (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 |
|
|
602
650
|
| (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 |
|
|
603
|
-
| (d) unit harness T1-
|
|
604
|
-
| (e) corporate-identifier scan | no forbidden identifiers in the published surface
|
|
605
|
-
| (f) non-English scan | no non-ASCII letters (`\p{L}` above U+007F) in
|
|
651
|
+
| (d) unit harness T1-T29 | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache, waste-detector, compaction-reserve, top-consumer, diff and subagent-rollup checks (75 total) | regressions in modelLimits, writeStateFile, hydrate, contributors, export, subagent lifetime |
|
|
652
|
+
| (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.tsx`, `lib/*`, `README.md`, `CHANGELOG.md`, `package.json`) or the test tree | compliance |
|
|
653
|
+
| (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 |
|
|
606
654
|
|
|
607
655
|
**Isolated E2E — `test/e2e-tui.mjs`** (requires the OpenCode CLI)
|
|
608
656
|
|
|
@@ -616,7 +664,10 @@ The E2E is fully **non-invasive** to your running OpenCode Desktop:
|
|
|
616
664
|
- `--standalone` starts a *private* server instead of the background service Desktop uses;
|
|
617
665
|
- `OPENCODE_DB` points at a throwaway DB in `%TEMP%` (your sessions are untouched);
|
|
618
666
|
- `OPENCODE_CONTEXT_INDICATOR_STATE_FILE` points at a throwaway state file in `%TEMP%`
|
|
619
|
-
|
|
667
|
+
- the live `opencode-context-indicator-state.json` is never read or written;
|
|
668
|
+
- `OPENCODE_CONTEXT_INDICATOR_LOG_FILE` does the same for the human-readable
|
|
669
|
+
`context-breakdown.log` (used by the unit gate, which drives idle/compacted
|
|
670
|
+
handlers);
|
|
620
671
|
- `OPENCODE_CONFIG_DIR` points at a throwaway global config written by the script
|
|
621
672
|
(plugins: this checkout; `update: "disable"`) **for Check A only** — the load
|
|
622
673
|
source is deterministic and fail-closed instead of following your live config;
|
package/docs/sidebar.png
ADDED
|
Binary file
|
package/index.js
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
* event-driven and does not need the removed API.
|
|
68
68
|
*
|
|
69
69
|
* V2 slash commands (added 1.1.0, verified against the live Desktop 2.0.19
|
|
70
|
-
* plugin runtime).
|
|
70
|
+
* plugin runtime). Three commands:
|
|
71
71
|
* - /context — a 3-line summary of THIS session only (model, tokens, %,
|
|
72
72
|
* updated + category line). Main only: NO subagent traversal and NO live
|
|
73
73
|
* RPC (cache + state.json only), so it is instant. Delivered with
|
|
@@ -78,7 +78,11 @@
|
|
|
78
78
|
* descendants), collected in parallel under one budget. Delivered with
|
|
79
79
|
* resume:true and a verbatim instruction prefix, so the agent echoes the
|
|
80
80
|
* table into the transcript as a normal message (rendered in full by the
|
|
81
|
-
* Desktop); resume:false remains a one-constant fallback.
|
|
81
|
+
* Desktop); resume:false remains a one-constant fallback. Includes the
|
|
82
|
+
* top-5 heaviest single context consumers (main).
|
|
83
|
+
* - /context-diff — the previous distinct measurement (ctx + per-category
|
|
84
|
+
* deltas) vs the current one, main only, cache + state.json only. Also
|
|
85
|
+
* delivered with resume:true under a <context-diff> tag.
|
|
82
86
|
* - ctx.command.transform(editor => editor.add({ name, description,
|
|
83
87
|
* execute })) — registers a slash command; `execute` receives the
|
|
84
88
|
* invocation { sessionID, prompt:{text,files,agents,skills}, delivery }.
|
|
@@ -123,8 +127,8 @@
|
|
|
123
127
|
*
|
|
124
128
|
* - Metric: the native opencode overflow count — tokens.total when present,
|
|
125
129
|
* else input + output + cache.read + cache.write — compared against the
|
|
126
|
-
* model's compaction ceiling (opencode
|
|
127
|
-
* limit.input || limit.context, reserve = max(
|
|
130
|
+
* model's compaction ceiling (opencode 2.1.4 calculateCeiling): window =
|
|
131
|
+
* limit.input || limit.context, reserve = max(15% of window, 16k when
|
|
128
132
|
* window >= 32k), usable = window - reserve. Percentages are NOT
|
|
129
133
|
* clamped at 100% - exceeding the ceiling must stay visible.
|
|
130
134
|
* - Category estimates use a unicode-aware heuristic (see estimateTokens):
|
|
@@ -154,11 +158,13 @@ import { handleV2Event, onV2Context, onV2Compaction, estimateBreakdownFromContex
|
|
|
154
158
|
import { breakdownCategories, computeReclaim, contentFingerprint, estimateTokens, extractToolTarget, isSafeSessionKey, safeCell, safeReclaim, safeSaneOrNull, safeToolOutByTool, sumCategories, toolOutEntries } from "./lib/estimate.js";
|
|
155
159
|
import {
|
|
156
160
|
commandContextBreakdown,
|
|
161
|
+
commandContextDiff,
|
|
157
162
|
commandContextSummary,
|
|
158
163
|
CONTEXT_BREAKDOWN_RESUME,
|
|
159
164
|
collectSessionReport,
|
|
160
165
|
renderSessionRow,
|
|
161
166
|
renderBreakdownTable,
|
|
167
|
+
renderContextDiff,
|
|
162
168
|
shortSummaryText,
|
|
163
169
|
shortSummaryDescription,
|
|
164
170
|
} from "./lib/commands.js";
|
|
@@ -197,6 +203,8 @@ export {
|
|
|
197
203
|
collectSessionReport,
|
|
198
204
|
renderSessionRow,
|
|
199
205
|
renderBreakdownTable,
|
|
206
|
+
renderContextDiff,
|
|
207
|
+
commandContextDiff,
|
|
200
208
|
shortSummaryText,
|
|
201
209
|
shortSummaryDescription,
|
|
202
210
|
ensureModelLimits,
|
|
@@ -276,8 +284,9 @@ export default {
|
|
|
276
284
|
logErr("model limits prefetch failed", err),
|
|
277
285
|
);
|
|
278
286
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
287
|
+
// Three V2 slash commands (V2 only): /context (instant per-session
|
|
288
|
+
// summary), /context-breakdown (full table, main + all subagent
|
|
289
|
+
// descendants), /context-diff (delta vs the previous measurement).
|
|
281
290
|
// Guarded: older 2.0.x builds may lack ctx.command / ctx.session.synthetic —
|
|
282
291
|
// registering a command whose handler cannot deliver would fail silently.
|
|
283
292
|
try {
|
|
@@ -306,6 +315,13 @@ export default {
|
|
|
306
315
|
name: "context-breakdown",
|
|
307
316
|
}),
|
|
308
317
|
});
|
|
318
|
+
editor.add({
|
|
319
|
+
name: "context-diff",
|
|
320
|
+
description:
|
|
321
|
+
"What changed in the context since the previous measurement (main session)",
|
|
322
|
+
execute: (invocation) =>
|
|
323
|
+
commandContextDiff(ctx, invocation, { name: "context-diff" }),
|
|
324
|
+
});
|
|
309
325
|
} catch (err) {
|
|
310
326
|
logErr("v2 command transform failed", err);
|
|
311
327
|
}
|
package/lib/cache.js
CHANGED
|
@@ -120,9 +120,15 @@ function lastKnownCacheWrite(sessionID) {
|
|
|
120
120
|
// Clear per-session tracking. Full wipe on session.idle / session.deleted;
|
|
121
121
|
// on session.compacted (keepToastDedup=true) the toast-dedup entries survive
|
|
122
122
|
// the cleanup so consecutive compacted events still dedup.
|
|
123
|
+
//
|
|
124
|
+
// `keepSubagentRollup` preserves THIS session's parent attribution and token
|
|
125
|
+
// totals. A finished subagent goes idle while its parent is still running; the
|
|
126
|
+
// parent's live roll-up must keep counting that child, so the V2 idle handler
|
|
127
|
+
// passes `true` for children and lets the parent's own idle/deleted wipe them
|
|
128
|
+
// (see clearChildSubagentTracking).
|
|
123
129
|
// ---------------------------------------------------------------------------
|
|
124
130
|
|
|
125
|
-
function clearSessionTracking(sessionID, keepToastDedup = false) {
|
|
131
|
+
function clearSessionTracking(sessionID, keepToastDedup = false, keepSubagentRollup = false) {
|
|
126
132
|
if (!sessionID) return;
|
|
127
133
|
lastShown.delete(sessionID);
|
|
128
134
|
breakdownCache.delete(sessionID);
|
|
@@ -134,7 +140,7 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
|
|
|
134
140
|
lastCompactedToast.delete(sessionID);
|
|
135
141
|
}
|
|
136
142
|
// Attribution cache (re-resolved on the next toast if needed).
|
|
137
|
-
sessionParentCache.delete(sessionID);
|
|
143
|
+
if (!keepSubagentRollup) sessionParentCache.delete(sessionID);
|
|
138
144
|
// Idle-summary caches were previously left dirty — clean them too.
|
|
139
145
|
lastKnownModelCache.delete(sessionID);
|
|
140
146
|
lastKnownProviderCache.delete(sessionID);
|
|
@@ -145,7 +151,7 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
|
|
|
145
151
|
lastKnownCacheWriteCache.delete(sessionID);
|
|
146
152
|
lastBdWrite.delete(sessionID);
|
|
147
153
|
// V2 subagent accumulation for this session.
|
|
148
|
-
sessionTotals.delete(sessionID);
|
|
154
|
+
if (!keepSubagentRollup) sessionTotals.delete(sessionID);
|
|
149
155
|
// V2 agent label + context-fallback throttle (previously left dirty).
|
|
150
156
|
sessionAgentCache.delete(sessionID);
|
|
151
157
|
v2ContextFetchTimes.delete(sessionID);
|
|
@@ -155,6 +161,33 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
|
|
|
155
161
|
lastCompactionHookAt.delete(sessionID);
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
// Drop the roll-up entries of every descendant of `rootID` (attribution plus
|
|
165
|
+
// accumulated totals). Called when a session itself goes idle or is deleted:
|
|
166
|
+
// until then, finished children stay in the cache so the live roll-up keeps
|
|
167
|
+
// reporting them instead of collapsing to "0 session(s)" the moment a child
|
|
168
|
+
// finishes. Breadth-first, because a sub-subagent hangs off a child and a
|
|
169
|
+
// single-level sweep would leave it as a permanent orphan. Deletion during Map
|
|
170
|
+
// iteration is well-defined in JS.
|
|
171
|
+
function clearChildSubagentTracking(rootID) {
|
|
172
|
+
if (!rootID) return;
|
|
173
|
+
const queue = [rootID];
|
|
174
|
+
const seen = new Set(queue);
|
|
175
|
+
while (queue.length > 0) {
|
|
176
|
+
const parent = queue.shift();
|
|
177
|
+
for (const [childID, parentID] of sessionParentCache) {
|
|
178
|
+
if (parentID !== parent) continue;
|
|
179
|
+
// Full per-session wipe, not just the two roll-up maps: the child's
|
|
180
|
+
// lastKnown*/breakdown/tool-schema caches would otherwise linger until
|
|
181
|
+
// FIFO eviction and never be updated again.
|
|
182
|
+
clearSessionTracking(childID);
|
|
183
|
+
if (!seen.has(childID)) {
|
|
184
|
+
seen.add(childID);
|
|
185
|
+
queue.push(childID);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
158
191
|
export {
|
|
159
192
|
MAX_TRACKED_SESSIONS,
|
|
160
193
|
BREAKDOWN_THROTTLE_MS,
|
|
@@ -190,4 +223,5 @@ export {
|
|
|
190
223
|
lastKnownCacheRead,
|
|
191
224
|
lastKnownCacheWrite,
|
|
192
225
|
clearSessionTracking,
|
|
226
|
+
clearChildSubagentTracking,
|
|
193
227
|
};
|