opencode-context-indicator 1.6.0 → 1.7.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 +85 -3
- package/README.md +144 -43
- package/dist/gui-experimental.cjs +1 -0
- package/index.js +16 -5
- package/lib/commands.js +81 -18
- package/lib/desktop.js +102 -0
- package/lib/estimate.js +31 -2
- package/lib/format.js +19 -0
- package/lib/limits.js +16 -11
- package/lib/rpc.d.ts +47 -0
- package/lib/rpc.js +88 -0
- package/lib/state.js +200 -56
- package/lib/tui-data.js +5 -9
- package/lib/v1.js +9 -2
- package/lib/v2.js +140 -15
- package/package.json +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,86 @@ 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.7.0 — 2026-10-29
|
|
8
|
+
|
|
9
|
+
Desktop GUI release (experimental): the plugin now has a surface in the
|
|
10
|
+
OpenCode Desktop app, not just in the terminal.
|
|
11
|
+
|
|
12
|
+
- **Desktop side-panel tab.** A new `./gui-experimental` part contributes a
|
|
13
|
+
"Context" tab to the session side region: model, `ctx / usable (percent)`, the
|
|
14
|
+
eight categories as value + bar + share, the measurement time and source tag,
|
|
15
|
+
a stale-after-compaction marker, the compaction reserve, and one line per
|
|
16
|
+
subagent with its own window and percentage. Opened from the `+` menu of the
|
|
17
|
+
side region, the command palette, or the `/cx` slash command.
|
|
18
|
+
- **Composer pill.** A small always-on `NN% ctx` capsule above the composer,
|
|
19
|
+
amber from 65 % and red from 85 % of the usable window; clicking it opens the
|
|
20
|
+
tab. It hides itself while the server part is unavailable instead of showing a
|
|
21
|
+
stale number.
|
|
22
|
+
- **New public RPC contract (`./rpc`).** A Desktop window has no filesystem
|
|
23
|
+
access, so the panel cannot read `state.json`; the server part now registers
|
|
24
|
+
an RPC contract (id `context-indicator`, method `family`) that returns the root
|
|
25
|
+
session plus every descendant as flat rows, and the GUI polls it through
|
|
26
|
+
`ctx.client.rpc()`. The contract is exported so other plugins and clients can
|
|
27
|
+
read the same data.
|
|
28
|
+
- **One family walk instead of two.** `/context-breakdown` and the RPC now share
|
|
29
|
+
`collectFamilyReports()`: the descendant BFS, the shared wall-clock budget and
|
|
30
|
+
the per-session collection exist once. The RPC uses a 2 s budget (a poll runs
|
|
31
|
+
far more often than a command).
|
|
32
|
+
- **Experimental warning.** The host GUI API (`@opencode/plugin/gui`) is marked
|
|
33
|
+
experimental upstream and may change between releases; if it does, the GUI
|
|
34
|
+
part stops loading while the server part, the slash commands and the TUI
|
|
35
|
+
surfaces keep working. The Desktop part is not covered by CI (it needs a real
|
|
36
|
+
window); CI typechecks it, builds the bundle and validates the artifact.
|
|
37
|
+
- **Fixed: hook-measured categories are authoritative in `state.json`.** The
|
|
38
|
+
file is shared by every plugin instance, and the Desktop window's instance
|
|
39
|
+
never serves a model request, so its breakdown is always the client-context
|
|
40
|
+
fallback (no system prompt, no tool schemas). It used to overwrite the
|
|
41
|
+
hook-measured categories milliseconds after the hook wrote them, which is what
|
|
42
|
+
made the panel show `system 0` / `tool schemas 0` / a grossly inflated `other`
|
|
43
|
+
and could blank `user`/`assistant`. Entries now carry their `source`, and only
|
|
44
|
+
a hook breakdown may replace hook numbers (a fallback still fills a session
|
|
45
|
+
that no hook has ever measured). The same rule now applies to the command
|
|
46
|
+
report and to the live log, so all three surfaces agree.
|
|
47
|
+
- **Fixed: the stale-after-compaction mark survives a fallback write.** A
|
|
48
|
+
conversation rebuilt from the persisted messages is not a measurement of the
|
|
49
|
+
compacted context; only a fresh hook breakdown clears the mark now, so the
|
|
50
|
+
panel keeps saying "stale after compaction" instead of silently dropping it.
|
|
51
|
+
The compaction hook also marks the entry as "already handled" only when it
|
|
52
|
+
actually persisted a measurement, so a hook instance with a cold cache no
|
|
53
|
+
longer suppresses the fallback and makes a compaction leave no trace at all.
|
|
54
|
+
- **Fixed: compaction notes could vanish from the log.** `final summaries` lived
|
|
55
|
+
in per-process memory while the log file is shared, so the next snapshot
|
|
56
|
+
rewrite from another instance wiped a `COMPACTION`/`COMPACTED` note that had
|
|
57
|
+
just been appended. The writer now merges the section already on disk
|
|
58
|
+
(block-level, de-duplicated, bounded) under its own lock, and the live snapshot
|
|
59
|
+
prints a hook-measured `state.json` entry — categories, residual `other` and
|
|
60
|
+
its `ctx`/`input` counters, tagged `snapshot=<updatedAt>` — instead of this
|
|
61
|
+
instance's fallback-shaped view. Repeated compactions of one session are also
|
|
62
|
+
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.
|
|
71
|
+
- Dev: `lib/format.js` (pure `fmt`/`num`) extracted so the GUI bundle has no
|
|
72
|
+
`node:*` import; gate (a) now expands `dist/`, gate (e)/(f) scan `gui/` and the
|
|
73
|
+
GUI build config, new gate (g) validates the built CJS bundle (app-provided
|
|
74
|
+
modules kept external, no React/OpenTUI/bare-`opencode` import, no Node
|
|
75
|
+
built-in, no ESM or dynamic import left), new unit checks T30 (schema vs row
|
|
76
|
+
builder), T31 (RPC registration + handler), T32 (fallback vs hook priority in
|
|
77
|
+
the report), T33 (fallback vs hook priority in the shared state file, stale
|
|
78
|
+
mark), T34 (summary merge across instances), T35 (hook-family authority for
|
|
79
|
+
the compaction hook, the V1 transform hooks and legacy untagged entries, plus
|
|
80
|
+
the live-log substitution) and T25c/T25d (a cold-cache or lock-refused
|
|
81
|
+
compaction hook must not arm the trailing-event guard), 85 checks total; CI
|
|
82
|
+
builds the GUI and typechecks it before the gate. The isolated e2e (plugin
|
|
83
|
+
load + state write) passes on both the 2.2.0 CLI and the 2.2.1 Desktop CLI;
|
|
84
|
+
the dev dependency stays pinned to `@opencode/plugin@2.2.0` for CI
|
|
85
|
+
reproducibility.
|
|
86
|
+
|
|
7
87
|
## 1.6.0 — 2026-10-27
|
|
8
88
|
|
|
9
89
|
Host alignment release: opencode 2.2.0 moved TUI plugins to OpenTUI 0.7.
|
|
@@ -20,7 +100,8 @@ Host alignment release: opencode 2.2.0 moved TUI plugins to OpenTUI 0.7.
|
|
|
20
100
|
opencode 2.0.16+. The TUI module now ships compiled against OpenTUI 0.7, so
|
|
21
101
|
the sidebar and the `/cx` panel require **opencode ≥ 2.2.0** and cannot load on
|
|
22
102
|
an older host. Older-host installation was not re-verified for this release.
|
|
23
|
-
- **Compaction ceiling unchanged
|
|
103
|
+
- **Compaction ceiling unchanged (superseded — see 1.7.0).**
|
|
104
|
+
`calculateCeiling()` in 2.2.0 is identical
|
|
24
105
|
to 2.1.x, so the `0.15 / 16k / 32k` reserve formula and every `%` denominator
|
|
25
106
|
are unaffected. (2.2.0 changed how a *compaction request* is sized - measured
|
|
26
107
|
prompt instead of characters - not the trigger.)
|
|
@@ -67,9 +148,10 @@ Fix-and-hygiene release after a full-project review. No new commands.
|
|
|
67
148
|
|
|
68
149
|
## 1.5.3 — 2026-10-26
|
|
69
150
|
|
|
70
|
-
- **Compaction ceiling aligned with opencode 2.1.4
|
|
151
|
+
- **Compaction ceiling aligned with opencode 2.1.4.** `lib/limits.js`
|
|
71
152
|
now mirrors `calculateCeiling()` exactly: the reserve is
|
|
72
|
-
`max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)` (was 10
|
|
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),
|
|
73
155
|
and an override buffer is honored **only while it is smaller than the
|
|
74
156
|
window** — a buffer configured for a larger model is ignored instead of
|
|
75
157
|
leaving zero room. The `compact at` line and the `%` denominator now match the
|
package/README.md
CHANGED
|
@@ -13,8 +13,10 @@ 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
|
|
17
|
-
|
|
16
|
+
and a redacted export, and an **experimental Desktop GUI part**
|
|
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
20
|
|
|
19
21
|
> **V1 is feature-frozen.** It is kept working for compatibility (bug fixes
|
|
20
22
|
> only); all new features land in the V2 path.
|
|
@@ -70,11 +72,11 @@ For every served request the plugin estimates how the context window is split:
|
|
|
70
72
|
duplicate group), reported honestly as *"N redundant tool outputs ≈ X tokens"*
|
|
71
73
|
— never as "the model wasted effort". Findings are per-session and **path-free
|
|
72
74
|
in the persisted/exported form**.
|
|
73
|
-
* The denominator is the **opencode 2.2.
|
|
74
|
-
|
|
75
|
-
limit.input || limit.context`; `reserve = max(floor(window * 0.15), window >=
|
|
75
|
+
* The denominator is the **opencode 2.2.x compaction ceiling**: `window =
|
|
76
|
+
limit.input || limit.context`; `reserve = max(floor(window * 0.10), window >=
|
|
76
77
|
2 * 16_000 ? 16_000 : 0)`; the ceiling is `window - reserve`. This mirrors
|
|
77
|
-
`calculateCeiling()` in `packages/core/src/session/compaction.ts
|
|
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
|
|
78
80
|
carries a `compaction.buffer` configured in its own settings, the plugin
|
|
79
81
|
cannot read that value (it is not exposed to plugins), so the default
|
|
80
82
|
reserve formula is used — set `OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER`
|
|
@@ -372,14 +374,95 @@ an error toast.
|
|
|
372
374
|
|
|
373
375
|
---
|
|
374
376
|
|
|
377
|
+
## Desktop panel (experimental)
|
|
378
|
+
|
|
379
|
+
OpenCode Desktop has no TUI, so the sidebar above cannot exist there. The
|
|
380
|
+
package therefore ships a **GUI part** (`./gui-experimental`) that renders the
|
|
381
|
+
same numbers as a Desktop **side-panel tab**, plus a **composer pill** with the
|
|
382
|
+
current percentage.
|
|
383
|
+
|
|
384
|
+
> **Experimental.** The host API it uses (`@opencode/plugin/gui`, opencode
|
|
385
|
+
> 2.2.0+) is explicitly marked experimental upstream and may change between
|
|
386
|
+
> releases; the export name `./gui-experimental` says so. If the host changes
|
|
387
|
+
> the API, the GUI part fails to load while the server part, the slash commands
|
|
388
|
+
> and the TUI surfaces keep working.
|
|
389
|
+
|
|
390
|
+
### What it shows
|
|
391
|
+
|
|
392
|
+
**Context tab** (side region): the model, `ctx / usable (percent)`, the eight
|
|
393
|
+
categories as value + bar + share of the context, the measurement time, the
|
|
394
|
+
`source` tag (`live` / `snapshot` / `client-context` / `no-data`), a
|
|
395
|
+
`stale after compaction` marker, the compaction reserve, and one line per
|
|
396
|
+
subagent with its own window and percentage.
|
|
397
|
+
|
|
398
|
+
**Composer pill**: `38% ctx` above the composer, amber from 65 % and red from
|
|
399
|
+
85 % of the usable window; clicking it opens the panel.
|
|
400
|
+
|
|
401
|
+
### Opening it
|
|
402
|
+
|
|
403
|
+
| Way | Where |
|
|
404
|
+
| --- | --- |
|
|
405
|
+
| `+` menu of the side region | "Context" |
|
|
406
|
+
| Command palette / slash command `/cx` | "Open context panel" |
|
|
407
|
+
| Clicking the composer pill | opens the tab for the current session |
|
|
408
|
+
|
|
409
|
+
### Where the numbers come from
|
|
410
|
+
|
|
411
|
+
A Desktop window has **no filesystem access**, so it cannot read `state.json`
|
|
412
|
+
the way the TUI does. The server part registers an **RPC contract**
|
|
413
|
+
(`lib/rpc.js`, id `context-indicator`, method `family`) that answers with the
|
|
414
|
+
root session plus every descendant, and the GUI calls it through
|
|
415
|
+
`ctx.client.rpc()`. The panel **polls** every 1.5 s (the pill every 5 s) instead
|
|
416
|
+
of subscribing to events: the contract stays one method, a poll costs no provider
|
|
417
|
+
call (the server answers from `state.json` plus its in-memory caches once the
|
|
418
|
+
session has been measured; a session this instance never observed live costs at
|
|
419
|
+
most one `session.get()` per instance, not per poll), and polling recovers by
|
|
420
|
+
itself when the desktop app's background server restarts.
|
|
421
|
+
|
|
422
|
+
The contract is published as the `./rpc` export, so other plugins and clients
|
|
423
|
+
can read the same data:
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
import { DesktopContextRpc } from "opencode-context-indicator/rpc"
|
|
427
|
+
const { rows } = await client.rpc(DesktopContextRpc).family({ sessionID })
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Install
|
|
431
|
+
|
|
432
|
+
The Desktop part is loaded from the same plugin entry, nothing extra to
|
|
433
|
+
configure:
|
|
434
|
+
|
|
435
|
+
```jsonc
|
|
436
|
+
{
|
|
437
|
+
"$schema": "https://opencode.ai/config.json",
|
|
438
|
+
"plugins": ["opencode-context-indicator"]
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
After installing, restart the app and check **Settings > Extensions**; the
|
|
443
|
+
plugin must be listed with its GUI part loaded. The `dist/gui-experimental.cjs`
|
|
444
|
+
bundle in the package is prebuilt, so no build step is needed on install.
|
|
445
|
+
|
|
446
|
+
### Limits
|
|
447
|
+
|
|
448
|
+
* **Desktop only.** The TUI has its own sidebar and `/cx` panel, and loading a
|
|
449
|
+
GUI bundle there does nothing.
|
|
450
|
+
* **Polling, not push.** A change can take up to 1.5 s to appear (5 s for the
|
|
451
|
+
pill); the numbers themselves are the same snapshot the sidebar reads.
|
|
452
|
+
* **Not exercised in CI.** The GUI needs a real Desktop window: CI typechecks
|
|
453
|
+
`gui/*.tsx`, builds the bundle and validates the artifact (gate `g`), and the
|
|
454
|
+
panel and the pill are verified manually in the app before a release.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
375
458
|
## Surface matrix
|
|
376
459
|
|
|
377
|
-
| Surface | Toast | TUI sidebar | TUI panel (/cx) | Breakdown log file | Slash commands |
|
|
378
|
-
| --- | :---: | :---: | :---: | :---: | :---: |
|
|
379
|
-
| OpenCode **1.x** — Desktop | ✅ | — | — | ✅ | — |
|
|
380
|
-
| OpenCode **1.x** — TUI | ✅ | — | — | ✅ | — |
|
|
381
|
-
| OpenCode **2.x** — TUI | — | ✅ | ✅ | ✅ | ✅ |
|
|
382
|
-
| OpenCode **2.x** — Desktop | — | — | — | ✅ | ✅ |
|
|
460
|
+
| Surface | Toast | TUI sidebar | TUI panel (/cx) | Desktop panel | Desktop pill | Breakdown log file | Slash commands |
|
|
461
|
+
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
|
462
|
+
| OpenCode **1.x** — Desktop | ✅ | — | — | — | — | ✅ | — |
|
|
463
|
+
| OpenCode **1.x** — TUI | ✅ | — | — | — | — | ✅ | — |
|
|
464
|
+
| OpenCode **2.x** — TUI | — | ✅ | ✅ | — | — | ✅ | ✅ |
|
|
465
|
+
| OpenCode **2.x** — Desktop | — | — | — | ✅ (exp) | ✅ (exp) | ✅ | ✅ |
|
|
383
466
|
|
|
384
467
|
Notes — these reflect what the plugin API actually exposes today:
|
|
385
468
|
|
|
@@ -394,8 +477,13 @@ Notes — these reflect what the plugin API actually exposes today:
|
|
|
394
477
|
* **V1 has no sidebar and no slash command.** The command/slot API (and the
|
|
395
478
|
`./tui` entry) is a V2 feature; OpenCode 1.x has no equivalent, so V1 is
|
|
396
479
|
toast + log only.
|
|
397
|
-
* The Desktop app is TUI-less
|
|
398
|
-
`/context` / `/context-breakdown` / `/context-diff` slash commands
|
|
480
|
+
* The Desktop app is TUI-less. Its V2 surfaces are the log file, the
|
|
481
|
+
`/context` / `/context-breakdown` / `/context-diff` slash commands, and the
|
|
482
|
+
**experimental** GUI part: a side-panel tab and a composer pill fed by the
|
|
483
|
+
plugin over RPC (see
|
|
484
|
+
[Desktop panel (experimental)](#desktop-panel-experimental)). That part needs
|
|
485
|
+
a host that loads `./gui-experimental` (opencode 2.2.0+); without it the log
|
|
486
|
+
file and the slash commands still work.
|
|
399
487
|
|
|
400
488
|
---
|
|
401
489
|
|
|
@@ -407,24 +495,22 @@ and `$TMPDIR` (usually `/tmp`) elsewhere:
|
|
|
407
495
|
| File | Purpose |
|
|
408
496
|
| --- | --- |
|
|
409
497
|
| `context-breakdown.log` | Human-readable live snapshot + bounded final summaries |
|
|
410
|
-
| `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`. |
|
|
498
|
+
| `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}`, `source` (provenance of the categories: `context-hook` / `compaction-hook` (V2) / `transform` / `system-transform` (V1) / `client-context` / `snapshot` - any hook-family source wins, a fallback never overwrites it), `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`. |
|
|
411
499
|
| `context-events.log` | Raw event tap — **only** when `DEBUG_EVENTS` is flipped to `true` in the source (off by default) |
|
|
412
500
|
|
|
413
501
|
The whole `context-breakdown.log` is **rewritten atomically** (temp file +
|
|
414
502
|
rename, like the state file): the live snapshot is always regenerated and the
|
|
415
503
|
final summaries (on `session.idle`) plus compaction notes are re-rendered from
|
|
416
|
-
an in-memory buffer (bounded, last 50 per
|
|
417
|
-
concurrent plugin instances cannot observe a half-written file. A
|
|
418
|
-
write (idle, compaction, command payload) re-renders the last live
|
|
419
|
-
adding a note never blanks that section. That snapshot may predate a
|
|
420
|
-
or a deleted session - the leading `[<timestamp>]` line is the
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
summaries are prevented separately by an atomic claim marker (see
|
|
427
|
-
`lib/dedup.js`).
|
|
504
|
+
an in-memory buffer (bounded, last 50 blocks per instance), so the file never
|
|
505
|
+
grows and concurrent plugin instances cannot observe a half-written file. A
|
|
506
|
+
summary-only write (idle, compaction, command payload) re-renders the last live
|
|
507
|
+
snapshot, so adding a note never blanks that section. That snapshot may predate a
|
|
508
|
+
compaction or a deleted session - the leading `[<timestamp>]` line is the
|
|
509
|
+
freshness cue. Each instance keeps its own buffer, so every write **merges the
|
|
510
|
+
summary section already on disk** (de-duplicated by line, bounded to 400 lines):
|
|
511
|
+
a note appended by the instance that handled the event survives the next
|
|
512
|
+
snapshot rewrite from another instance. Cross-instance duplicate summaries are
|
|
513
|
+
prevented separately by an atomic claim marker (see `lib/dedup.js`).
|
|
428
514
|
|
|
429
515
|
---
|
|
430
516
|
|
|
@@ -548,10 +634,11 @@ the sidebar instead.
|
|
|
548
634
|
gracefully (the breakdown falls back to throttled `session.messages` fetches
|
|
549
635
|
and toasts keep working).
|
|
550
636
|
* **OpenCode ≥ 2.2.0** for the V2 path with the TUI surfaces (built and verified
|
|
551
|
-
against **2.2.0
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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.
|
|
555
642
|
* **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
|
|
556
643
|
ships with OpenCode. `@opentui/core` and `solid-js` are **optional** peer
|
|
557
644
|
dependencies resolved by OpenCode at runtime; the main plugin installs and runs
|
|
@@ -565,6 +652,12 @@ the sidebar instead.
|
|
|
565
652
|
upstream limitation, on the **npm** path the sidebar is **auto-disabled**
|
|
566
653
|
(the plugin logs one hint and registers no slot) — use a `file://` install for
|
|
567
654
|
the live sidebar, see [Live sidebar: npm vs file:// install](#live-sidebar-npm-vs-file-install).
|
|
655
|
+
* **Desktop GUI part** (optional, experimental): needs an OpenCode Desktop
|
|
656
|
+
release that loads the `./gui-experimental` export (opencode 2.2.0+, where the
|
|
657
|
+
GUI plugin API exists). The bundle is prebuilt and ships in the package, so it
|
|
658
|
+
adds **no** runtime dependency; `solid-js`, the GUI SDK and the client are
|
|
659
|
+
provided by the app itself. On hosts without that API the export is simply not
|
|
660
|
+
loaded and everything else keeps working.
|
|
568
661
|
|
|
569
662
|
---
|
|
570
663
|
|
|
@@ -630,15 +723,17 @@ caller.
|
|
|
630
723
|
### Running tests
|
|
631
724
|
|
|
632
725
|
```bash
|
|
633
|
-
npm run
|
|
726
|
+
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
|
|
728
|
+
npm run test:types # tsc for the TUI entry AND the Desktop GUI part
|
|
634
729
|
npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
|
|
635
730
|
npm run test:e2e:quick # E2E load-only, no LLM call
|
|
636
731
|
npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
|
|
637
732
|
npm run test:drift:strict # release check: fails on ANY host drift
|
|
638
733
|
```
|
|
639
734
|
|
|
640
|
-
`prepublishOnly` runs **
|
|
641
|
-
(`node test/prepublish.mjs && node test/e2e-tui.mjs --full`) and is skipped with
|
|
735
|
+
`prepublishOnly` runs **all** of it automatically on `npm publish`
|
|
736
|
+
(`npm run build:gui && node test/prepublish.mjs && node test/e2e-tui.mjs --full`) and is skipped with
|
|
642
737
|
`--ignore-scripts`. The whole chain takes roughly **1 minute** (fast gate ~20 s,
|
|
643
738
|
isolated E2E ~10 s warm / up to ~90 s cold); the E2E is bounded by INIT 45 s +
|
|
644
739
|
STEP 80 s, ~3 min total.
|
|
@@ -649,12 +744,13 @@ STEP 80 s, ~3 min total.
|
|
|
649
744
|
|
|
650
745
|
| Step | What | Why |
|
|
651
746
|
|---|---|---|
|
|
652
|
-
| (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 |
|
|
747
|
+
| (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 |
|
|
653
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 |
|
|
654
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 |
|
|
655
|
-
| (d) unit harness T1-
|
|
656
|
-
| (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.tsx`, `lib/*`, `README.md`, `CHANGELOG.md`, `package.json
|
|
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 |
|
|
657
752
|
| (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
|
+
| (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 |
|
|
658
754
|
|
|
659
755
|
**Isolated E2E — `test/e2e-tui.mjs`** (requires the OpenCode CLI)
|
|
660
756
|
|
|
@@ -696,11 +792,13 @@ model.
|
|
|
696
792
|
and on a daily schedule (GitHub auto-parks scheduled workflows after 60 days of
|
|
697
793
|
repo inactivity — a push or manual run re-enables them):
|
|
698
794
|
|
|
699
|
-
1. `npx --no-install tsc --noEmit`
|
|
700
|
-
`lib/*` imports)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
3. `test/
|
|
795
|
+
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
|
|
797
|
+
Desktop GUI part (`gui/*.tsx`, Solid JSX);
|
|
798
|
+
2. `npm run build:gui` - build the Desktop GUI bundle before the gate inspects it;
|
|
799
|
+
3. `test/prepublish.mjs` - the full fast gate (a)-(g), including the standalone
|
|
800
|
+
esbuild transpile check (pragmas, no React) and the GUI bundle check;
|
|
801
|
+
4. `test/drift-watch.mjs` - compares the OpenTUI pins and the
|
|
704
802
|
`@opencode/plugin` types devDependency with the **latest released** opencode.
|
|
705
803
|
It is **advisory by default**: patch drift prints a `::warning::` annotation
|
|
706
804
|
and exits 0, so the daily pipeline does not go red on every host patch. Only
|
|
@@ -711,7 +809,10 @@ repo inactivity — a push or manual run re-enables them):
|
|
|
711
809
|
`npm run test:drift:strict` to fail on any drift.
|
|
712
810
|
|
|
713
811
|
The Node-pty E2E is not run in CI (requires the OpenCode binary) and is skipped
|
|
714
|
-
gracefully when the CLI is absent (`[SKIP] opencode CLI not found`).
|
|
812
|
+
gracefully when the CLI is absent (`[SKIP] opencode CLI not found`). The Desktop
|
|
813
|
+
GUI part cannot be exercised headless at all: CI typechecks it, builds the
|
|
814
|
+
bundle and checks the artifact, and the panel/pill are verified manually in a
|
|
815
|
+
real Desktop window before a release.
|
|
715
816
|
|
|
716
817
|
---
|
|
717
818
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js/web"),t=require("@opencode/plugin/gui"),n=require("solid-js"),r=require("@opencode/plugin/rpc");var i={sessionID:{type:`string`},parentID:{type:`string`},agent:{type:`string`},modelID:{type:`string`},providerID:{type:`string`},ctx:{type:`number`},input:{type:`number`},usable:{type:`number`},reserve:{type:`number`},user:{type:`number`},assistant:{type:`number`},reasoning:{type:`number`},toolArgs:{type:`number`},toolOut:{type:`number`},system:{type:`number`},toolSchemas:{type:`number`},other:{type:`number`},source:{type:`string`},updatedAt:{type:`string`},stale:{type:`boolean`}},a=Object.keys(i),o=r.Rpc.define({id:`context-indicator`,methods:{family:{input:{type:`object`,properties:{sessionID:{type:`string`}},required:[`sessionID`],additionalProperties:!1},output:{type:`object`,properties:{rows:{type:`array`,items:{type:`object`,properties:i,required:a,additionalProperties:!1}}},required:[`rows`],additionalProperties:!1}}},events:{}});function s(e){return Number.isFinite(e)?e>=1e3?`${(e/1e3).toFixed(1)}k`:String(Math.round(e)):`0`}var c=(0,e.template)(`<div>Server part unavailable: <!> (retrying)`),l=(0,e.template)(`<div>`),u=(0,e.template)(`<div>Loading context...`),d=(0,e.template)(`<div>No measurement yet. Send a prompt in this session and the numbers appear here.`),f=(0,e.template)(`<div><span></span><span>`),p=(0,e.template)(`<span>stale after compaction`),m=(0,e.template)(`<span>reserve `),h=(0,e.template)(`<div>Subagents (<!>)`),g=(0,e.template)(`<div><span></span><span></span><span><span style=display:block></span></span><span>`),_=1500,v=[[`system`,`System`],[`toolSchemas`,`Tool schemas`],[`user`,`User`],[`assistant`,`Assistant`],[`reasoning`,`Reasoning`],[`toolArgs`,`Tool args`],[`toolOut`,`Tool output`],[`other`,`Other`]];function y(e){if(!e)return`?`;try{let t=new Date(e);return Number.isNaN(t.getTime())?`?`:t.toLocaleTimeString()}catch{return`?`}}function b(e,t){return!t||t<=0?``:`${(e/t*100).toFixed(0)}%`}var x={panel:{display:`flex`,"flex-direction":`column`,gap:`10px`,padding:`12px`,"font-size":`12px`,"line-height":`1.45`},hint:{opacity:`0.65`,"font-size":`12px`},header:{display:`flex`,"align-items":`baseline`,"justify-content":`space-between`,gap:`8px`},model:{"font-weight":`600`,overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`},total:{"font-variant-numeric":`tabular-nums`,"white-space":`nowrap`},section:{display:`flex`,"flex-direction":`column`,gap:`6px`},row:{display:`grid`,"grid-template-columns":`1fr auto 64px auto`,"align-items":`center`,gap:`6px`},label:{opacity:`0.8`,overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`},value:{"font-variant-numeric":`tabular-nums`},track:{height:`4px`,"border-radius":`2px`,background:`currentColor`,opacity:`0.15`,overflow:`hidden`},fill:{height:`4px`,"border-radius":`2px`,background:`currentColor`,opacity:`0.9`},share:{opacity:`0.65`,"text-align":`right`,"font-variant-numeric":`tabular-nums`},meta:{display:`flex`,gap:`8px`,opacity:`0.6`,"font-size":`11px`},sub:{display:`flex`,"justify-content":`space-between`,gap:`8px`,"font-size":`11px`},subName:{overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`,opacity:`0.85`},title:{"font-weight":`600`,opacity:`0.85`}};function S(r){let i=(0,t.usePlugin)(),[a,S]=(0,n.createSignal)([]),[C,w]=(0,n.createSignal)(``),[T,E]=(0,n.createSignal)(!1);(0,n.createEffect)(()=>{let e=r.session.id,t=!1,a=!1;E(!1),S([]),w(``);let s=async()=>{if(!(a||t)){t=!0;try{let t=await i.client.rpc(o).family({sessionID:e});if(a)return;S(Array.isArray(t?.rows)?t.rows:[]),w(``)}catch(e){if(a)return;w(e instanceof Error?e.message:String(e))}finally{t=!1,a||E(!0)}}};s();let c=setInterval(()=>void s(),_);(0,n.onCleanup)(()=>{a=!0,clearInterval(c)})});let D=()=>a()[0],O=()=>a().slice(1),k=e=>{let t=D()?.ctx||0;return t<=0||e<=0?`0%`:`${Math.min(100,Math.max(2,Math.round(e/t*100)))}%`};return(()=>{var t=l();return(0,e.insert)(t,(0,e.createComponent)(n.Show,{get when(){return T()},get fallback(){return(()=>{var t=u();return(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t})()},get children(){return[(0,e.createComponent)(n.Show,{get when(){return C()},get children(){var t=c(),n=t.firstChild.nextSibling;return n.nextSibling,(0,e.insert)(t,C,n),(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t}}),(0,e.createComponent)(n.Show,{get when(){return D()},get fallback(){return(0,e.createComponent)(n.Show,{get when(){return!C()},get children(){var t=d();return(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t}})},children:t=>[(()=>{var n=f(),r=n.firstChild,i=r.nextSibling;return(0,e.insert)(r,()=>t().modelID||`unknown model`),(0,e.insert)(i,()=>s(t().ctx),null),(0,e.insert)(i,(()=>{var n=(0,e.memo)(()=>t().usable>0);return()=>n()?` / ${s(t().usable)} (${b(t().ctx,t().usable)})`:``})(),null),(0,e.effect)(a=>{var o=x.header,s=x.model,c=t().modelID,l=x.total;return a.e=(0,e.style)(n,o,a.e),a.t=(0,e.style)(r,s,a.t),c!==a.a&&(0,e.setAttribute)(r,`title`,a.a=c),a.o=(0,e.style)(i,l,a.o),a},{e:void 0,t:void 0,a:void 0,o:void 0}),n})(),(()=>{var r=l();return(0,e.insert)(r,(0,e.createComponent)(n.For,{each:v,children:n=>(()=>{var r=g(),i=r.firstChild,a=i.nextSibling,o=a.nextSibling,c=o.firstChild,l=o.nextSibling;return(0,e.insert)(i,()=>n[1]),(0,e.insert)(a,()=>s(t()[n[0]])),(0,e.insert)(l,()=>b(t()[n[0]],t().ctx)),(0,e.effect)(s=>{var u=x.row,d=x.label,f=x.value,p=x.track,m={...x.fill,width:k(t()[n[0]])},h=x.share;return s.e=(0,e.style)(r,u,s.e),s.t=(0,e.style)(i,d,s.t),s.a=(0,e.style)(a,f,s.a),s.o=(0,e.style)(o,p,s.o),s.i=(0,e.style)(c,m,s.i),s.n=(0,e.style)(l,h,s.n),s},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0}),r})()})),(0,e.effect)(t=>(0,e.style)(r,x.section,t)),r})(),(()=>{var r=f(),i=r.firstChild,a=i.nextSibling;return(0,e.insert)(i,()=>y(t().updatedAt)),(0,e.insert)(a,()=>t().source||`none`),(0,e.insert)(r,(0,e.createComponent)(n.Show,{get when(){return t().stale},get children(){return p()}}),null),(0,e.insert)(r,(0,e.createComponent)(n.Show,{get when(){return t().reserve>0},get children(){var n=m();return n.firstChild,(0,e.insert)(n,()=>s(t().reserve),null),n}}),null),(0,e.effect)(t=>(0,e.style)(r,x.meta,t)),r})(),(0,e.createComponent)(n.Show,{get when(){return O().length>0},get children(){return[(()=>{var t=h(),n=t.firstChild.nextSibling;return n.nextSibling,(0,e.insert)(t,()=>O().length,n),(0,e.effect)(n=>(0,e.style)(t,x.title,n)),t})(),(()=>{var t=l();return(0,e.insert)(t,(0,e.createComponent)(n.For,{get each(){return O()},children:t=>(()=>{var n=f(),r=n.firstChild,i=r.nextSibling;return(0,e.insert)(r,()=>t.agent||`sub`),(0,e.insert)(i,()=>s(t.ctx),null),(0,e.insert)(i,(()=>{var n=(0,e.memo)(()=>t.usable>0);return()=>n()?` (${b(t.ctx,t.usable)})`:``})(),null),(0,e.effect)(a=>{var o=x.sub,s=x.subName,c=t.modelID,l=x.value;return a.e=(0,e.style)(n,o,a.e),a.t=(0,e.style)(r,s,a.t),c!==a.a&&(0,e.setAttribute)(r,`title`,a.a=c),a.o=(0,e.style)(i,l,a.o),a},{e:void 0,t:void 0,a:void 0,o:void 0}),n})()})),(0,e.effect)(n=>(0,e.style)(t,x.section,n)),t})()]}})]})]}})),(0,e.effect)(n=>(0,e.style)(t,x.panel,n)),t})()}var C=`context`,w=`window-analytics`,T=`Context`,E=5e3,D=65,O=85,k={id:C,title:T},A=t.Plugin.define({setup(r){let i=`${r.id}:${C}`,a=e=>{let t=e??r.sessions.current();t&&r.layout.open(i,t,{tab:`select`})};r.add(t.Panel,()=>({id:C,region:`side`,wide:!0,list:e=>e.open.includes(C)?[k]:[],render:t=>(0,e.createComponent)(S,{get session(){return t.session}}),mobile:{title:T,order:1,kind:`tab`,icon:w}})),r.add(t.MenuItem,()=>({id:C,menu:`session.panel`,title:T,icon:w,description:`Context-window usage`,run:()=>a()})),r.add(t.Command,()=>({id:`open`,title:`Open context panel`,description:`Context-window usage for this session`,keywords:`context tokens window panel`,section:`session`,slash:{name:`cx`},run:()=>a()}));let[s,c]=(0,n.createSignal)(void 0),l=async()=>{let e=r.sessions.current();if(!e)c(void 0);else try{let t=(await r.client.rpc(o).family({sessionID:e.id}))?.rows?.[0];if(!t||t.usable<=0){c(void 0);return}let n=t.ctx/t.usable*100;c({sessionID:e.id,label:`${n.toFixed(0)}% ctx`,tone:n>=O?`danger`:n>=D?`warning`:void 0})}catch{c(void 0)}};l();let u=setInterval(()=>void l(),E);(0,n.onCleanup)(()=>clearInterval(u)),r.add(t.ComposerPill,()=>({id:`usage`,show:({session:e})=>{let t=s();if(t&&t.sessionID===e.id)return{label:t.label,icon:w,tone:t.tone,title:`Context window usage`,run:()=>a(e)}}}))}});exports.default=A;
|
package/index.js
CHANGED
|
@@ -127,10 +127,10 @@
|
|
|
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,
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
|
|
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
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,
|
|
@@ -177,7 +177,8 @@ import {
|
|
|
177
177
|
modelLimits,
|
|
178
178
|
reserveFor,
|
|
179
179
|
} from "./lib/limits.js";
|
|
180
|
-
import { lastKnownCacheRead, lastKnownCacheWrite, lastKnownCtxCache, lastKnownInputCache, lastKnownReasoningCache, sessionAgentCache, sessionParentCache } from "./lib/cache.js";
|
|
180
|
+
import { breakdownCache, lastKnownCacheRead, lastKnownCacheWrite, lastKnownCtxCache, lastKnownInputCache, lastKnownReasoningCache, sessionAgentCache, sessionParentCache } from "./lib/cache.js";
|
|
181
|
+
import { registerDesktopRpc, toDesktopRow } from "./lib/desktop.js";
|
|
181
182
|
import {
|
|
182
183
|
logErr,
|
|
183
184
|
readStateSnapshot,
|
|
@@ -229,9 +230,12 @@ export {
|
|
|
229
230
|
lastKnownCacheRead,
|
|
230
231
|
lastKnownCacheWrite,
|
|
231
232
|
lastKnownCtxCache,
|
|
233
|
+
breakdownCache,
|
|
232
234
|
lastKnownInputCache,
|
|
233
235
|
lastKnownReasoningCache,
|
|
234
236
|
handleV2Event,
|
|
237
|
+
registerDesktopRpc,
|
|
238
|
+
toDesktopRow,
|
|
235
239
|
};
|
|
236
240
|
|
|
237
241
|
// ---------------------------------------------------------------------------
|
|
@@ -337,6 +341,13 @@ export default {
|
|
|
337
341
|
logErr("v2 command transform registration failed", err);
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
// Desktop GUI data channel (experimental GUI plugins, opencode 2.2.0+).
|
|
345
|
+
// The GUI part ships as ./gui-experimental and calls this contract through
|
|
346
|
+
// ctx.client.rpc(); without it the panel has no data, so the registration is
|
|
347
|
+
// non-fatal and the TUI / slash commands keep working either way.
|
|
348
|
+
const desktopRpc = await registerDesktopRpc(ctx);
|
|
349
|
+
if (desktopRpc) registrations.push(desktopRpc);
|
|
350
|
+
|
|
340
351
|
const controller = new AbortController();
|
|
341
352
|
void (async () => {
|
|
342
353
|
try {
|