opencode-context-indicator 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ 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.1 — 2026-10-29
8
+
9
+ - **Fixed: the live log no longer prints `ctx=0` above real categories.** A hook
10
+ breakdown whose instance has no request counters (a mirror that only saw the
11
+ compaction, so `input`/`ctx` are still 0) used to be printed as-is: the header
12
+ contradicted the category lines below it. Such an entry now takes the same
13
+ path as a missing/fallback one and borrows the hook-measured `state.json`
14
+ snapshot whole (categories, counters, residual and a `snapshot=` stamp; a
15
+ snapshot without a timestamp is still marked `snapshot=unknown`). A counter is
16
+ knowledge only when it is positive: a borrowed entry whose own counters are 0,
17
+ or a ctx-only snapshot with no input, prints the unknown counter as `?`, and
18
+ the residual as `n/a (input unknown on this instance)` instead of a fabricated
19
+ `0` (`other = input - sum` needs an input). The same rule now applies to a
20
+ local hook entry that knows `ctx` but not `input`. T36 covers all four cases:
21
+ whole-entry borrow, no snapshot, zero-counter snapshot, ctx-only snapshot
22
+ (86 checks).
23
+ - **Fixed: the compaction reserve is 15 % again (1.7.0 regression).** 1.7.0
24
+ switched the denominator to a 10 % share on the strength of an unreleased
25
+ branch head. Every RELEASED host from 2.1.4 through 2.2.2 (2.1.4, 2.2.0,
26
+ 2.2.1, 2.2.2) still has `RESERVE_SHARE = 0.15` in
27
+ `packages/core/src/session/compaction.ts` (verified tag by tag), so on those
28
+ hosts 1.7.0 overstated the ceiling and the `compact at` line by ~5 % of the
29
+ window (~26 k tokens at 512 k) and therefore under-reported every `%`. The
30
+ plugin now mirrors `max(floor(window * 0.15), window >= 32_000 ? 16_000 : 0)`
31
+ again; the formula, the 16 k floor and the `2 * RESERVE_MIN` gate are
32
+ unchanged. Docs, the T16/T26 tables and the seed expectations are back on the
33
+ 15 % numbers.
34
+ - Dev: the isolated e2e (plugin load + state write) was re-run against the
35
+ **2.2.2** Desktop CLI (`opencode v2.2.2`); the Desktop 2.2.2 `app.asar` still
36
+ carries the `@opencode/plugin/gui` / `@opencode/plugin/rpc` bridge and the
37
+ `ComposerPill` / `Rpc.define` / `session.panel` symbols the experimental panel
38
+ uses, and `opencode-cli.exe` still exposes `opentui:runtime-module:` plus the
39
+ `node_modules` exclusion filter.
40
+
7
41
  ## 1.7.0 — 2026-10-29
8
42
 
9
43
  Desktop GUI release (experimental): the plugin now has a surface in the
@@ -60,14 +94,14 @@ OpenCode Desktop app, not just in the terminal.
60
94
  its `ctx`/`input` counters, tagged `snapshot=<updatedAt>` — instead of this
61
95
  instance's fallback-shaped view. Repeated compactions of one session are also
62
96
  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.
97
+ - **Reserve share (claim in 1.7.0 was wrong; corrected in 1.7.1).** This
98
+ release switched the denominator to a 10 % share; no host in the verified
99
+ range 2.1.4 through 2.2.2 used it. `calculateCeiling()` in those tags keeps
100
+ `RESERVE_SHARE = 0.15`, so 1.7.0 overstated the ceiling (and the `compact at`
101
+ line) by ~5 % of the window (~26 k tokens at 512 k) on them and therefore
102
+ under-reported every `%`. The 1.6.0 note that the ceiling was unchanged in
103
+ 2.2.0 was right (the formula and the `2 * RESERVE_MIN` gate did stay); the
104
+ share line here was the wrong part.
71
105
  - Dev: `lib/format.js` (pure `fmt`/`num`) extracted so the GUI bundle has no
72
106
  `node:*` import; gate (a) now expands `dist/`, gate (e)/(f) scan `gui/` and the
73
107
  GUI build config, new gate (g) validates the built CJS bundle (app-provided
@@ -100,11 +134,13 @@ Host alignment release: opencode 2.2.0 moved TUI plugins to OpenTUI 0.7.
100
134
  opencode 2.0.16+. The TUI module now ships compiled against OpenTUI 0.7, so
101
135
  the sidebar and the `/cx` panel require **opencode ≥ 2.2.0** and cannot load on
102
136
  an older host. Older-host installation was not re-verified for this release.
103
- - **Compaction ceiling unchanged (superseded — see 1.7.0).**
137
+ - **Compaction ceiling unchanged (this note was right; 1.7.0 briefly wrong).**
104
138
  `calculateCeiling()` in 2.2.0 is identical
105
139
  to 2.1.x, so the `0.15 / 16k / 32k` reserve formula and every `%` denominator
106
140
  are unaffected. (2.2.0 changed how a *compaction request* is sized - measured
107
- prompt instead of characters - not the trigger.)
141
+ prompt instead of characters - not the trigger. 1.7.0 later claimed the share
142
+ had dropped to 10 %; see 1.7.1 - no host in the verified range
143
+ 2.1.4-2.2.2 uses it.)
108
144
  - Dev: `drift-watch` is green again (host 2.2.0, OpenTUI 0.7.0); `tsc`,
109
145
  `prepublish` and the isolated e2e pass against the 2.2.0 CLI.
110
146
 
@@ -150,8 +186,9 @@ Fix-and-hygiene release after a full-project review. No new commands.
150
186
 
151
187
  - **Compaction ceiling aligned with opencode 2.1.4.** `lib/limits.js`
152
188
  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),
189
+ `max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)` (the plugin's
190
+ older builds used 10 %; no host in the verified range uses it - the share
191
+ stayed 0.15 there),
155
192
  and an override buffer is honored **only while it is smaller than the
156
193
  window** — a buffer configured for a larger model is ignored instead of
157
194
  leaving zero room. The `compact at` line and the `%` denominator now match the
package/README.md CHANGED
@@ -16,7 +16,7 @@ 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
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+.
19
+ re-run against the 2.2.1 and 2.2.2 Desktop CLI); the V1 path against 1.18.29+.
20
20
 
21
21
  > **V1 is feature-frozen.** It is kept working for compatibility (bug fixes
22
22
  > only); all new features land in the V2 path.
@@ -72,11 +72,13 @@ For every served request the plugin estimates how the context window is split:
72
72
  duplicate group), reported honestly as *"N redundant tool outputs ≈ X tokens"*
73
73
  — never as "the model wasted effort". Findings are per-session and **path-free
74
74
  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 >=
75
+ * The denominator is the **opencode 2.x compaction ceiling**: `window =
76
+ limit.input || limit.context`; `reserve = max(floor(window * 0.15), window >=
77
77
  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
78
+ `calculateCeiling()` in `packages/core/src/session/compaction.ts`, whose
79
+ `RESERVE_SHARE` is 0.15 in every released host from 2.1.4 through 2.2.2 (the
80
+ unreleased `v2` branch has a different share; the plugin follows the release,
81
+ not the branch). When opencode
80
82
  carries a `compaction.buffer` configured in its own settings, the plugin
81
83
  cannot read that value (it is not exposed to plugins), so the default
82
84
  reserve formula is used — set `OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER`
@@ -635,7 +637,8 @@ the sidebar instead.
635
637
  and toasts keep working).
636
638
  * **OpenCode ≥ 2.2.0** for the V2 path with the TUI surfaces (built and verified
637
639
  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
640
+ **2.2.2** Desktop CLI, the latest at the time of writing). The server-only V2
641
+ path keeps working on
639
642
  2.0.16+; the TUI module pins the OpenTUI generation it was compiled against,
640
643
  so a host on OpenTUI 0.6 (opencode ≤ 2.1.4) will not load the TUI surfaces -
641
644
  neither the sidebar nor the `/cx` panel - from a 1.6.x install.
@@ -724,7 +727,7 @@ caller.
724
727
 
725
728
  ```bash
726
729
  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
730
+ npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T36 unit checks + GUI bundle
728
731
  npm run test:types # tsc for the TUI entry AND the Desktop GUI part
729
732
  npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
730
733
  npm run test:e2e:quick # E2E load-only, no LLM call
@@ -747,7 +750,7 @@ STEP 80 s, ~3 min total.
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
751
  | (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
752
  | (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 |
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 |
751
754
  | (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 |
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 |
package/index.js CHANGED
@@ -127,10 +127,9 @@
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
130
+ * model's compaction ceiling (opencode calculateCeiling, released 2.x):
131
+ * window = limit.input || limit.context, reserve = max(15% of window, 16k
132
+ * when window >= 32k), usable = window - reserve. Percentages are NOT
134
133
  * clamped at 100% - exceeding the ceiling must stay visible.
135
134
  * - Category estimates use a unicode-aware heuristic (see estimateTokens):
136
135
  * Cyrillic ~2.5 chars/token, CJK ~1.5, latin/ASCII ~4. These are ESTIMATES,
package/lib/limits.js CHANGED
@@ -12,11 +12,11 @@
12
12
  // auto-compaction trigger ceiling as window - reserve, where
13
13
  // window = limit.input || limit.context (null/zero falls through to the context
14
14
  // window) and
15
- // reserve = max(floor(window * 0.10), window >= 2 * 16_000 ? 16_000 : 0)
16
- // The share was 15% up to v2.1.x and is 10% since v2.2.0 (PR #51271 "fit output
17
- // limits to the context window"); the `2 * RESERVE_MIN` gate is unchanged. A
18
- // wrong reserve makes every displayed percentage wrong, so this mirrors the
19
- // host exactly.
15
+ // reserve = max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)
16
+ // `RESERVE_SHARE = 0.15` in every RELEASED host from 2.1.4 through 2.2.2
17
+ // (verified tag by tag; the `2 * RESERVE_MIN` gate is older and unchanged). The
18
+ // unreleased `v2` branch HEAD has an inline `0.1` instead - do NOT mirror a
19
+ // branch head: a wrong reserve makes every displayed percentage wrong.
20
20
  // A compaction.buffer configured in opencode itself is NOT visible to the
21
21
  // plugin (the plugin cannot read opencode's internal compaction config); the
22
22
  // env var OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER overrides the reserve for
@@ -85,11 +85,11 @@ function getModelLimit(providerID, modelID) {
85
85
  }
86
86
 
87
87
  // Compute the reserve for a given window, matching opencode's
88
- // packages/core/src/session/compaction.ts calculateCeiling() in 2.2.x (the
89
- // share became 10% in 2.2.0; the 2 * RESERVE_MIN gate is older and unchanged).
88
+ // packages/core/src/session/compaction.ts calculateCeiling() as RELEASED
89
+ // (RESERVE_SHARE = 0.15 in 2.1.4 .. 2.2.2; the branch head is not a release).
90
90
  // Returns null when
91
91
  // window is not a finite positive number.
92
- const RESERVE_SHARE = 0.1; // core: 10% of the window (15% before v2.2.0)
92
+ const RESERVE_SHARE = 0.15; // core: RESERVE_SHARE in every released 2.x
93
93
  const RESERVE_MIN = 16_000; // core: RESERVE_MIN
94
94
  function reserveFor(window, bufferOverride) {
95
95
  if (
@@ -109,7 +109,7 @@ function reserveFor(window, bufferOverride) {
109
109
  return bufferOverride;
110
110
  }
111
111
  // Default reserve formula (mirror of opencode core):
112
- // max(floor(window * 0.10), window >= 2 * RESERVE_MIN ? RESERVE_MIN : 0)
112
+ // max(floor(window * 0.15), window >= 2 * RESERVE_MIN ? RESERVE_MIN : 0)
113
113
  // The threshold is 2 * RESERVE_MIN (32_000), not RESERVE_MIN.
114
114
  return Math.max(
115
115
  Math.floor(window * RESERVE_SHARE),
@@ -117,7 +117,7 @@ function reserveFor(window, bufferOverride) {
117
117
  );
118
118
  }
119
119
 
120
- // Native opencode compaction ceiling (calculateCeiling, core v2.2.x):
120
+ // Native opencode compaction ceiling (calculateCeiling, released 2.x):
121
121
  // window = limit.input || limit.context (zero/null falls through to context)
122
122
  // reserve = reserveFor(window) [or bufferOverride when provided]
123
123
  // usable = window - reserve
package/lib/v2.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  extractToolTarget,
16
16
  fmt,
17
17
  isHookMeasuredEntry,
18
+ isHookSource,
18
19
  isSafeSessionKey,
19
20
  oneLine,
20
21
  safeReclaim,
@@ -741,7 +742,21 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
741
742
  let snapInputTokens = null;
742
743
  let snapCtxTokens = null;
743
744
  let snapAt = null;
744
- if (!bd || bd.source === "client-context") {
745
+ let borrowedSnapshot = false;
746
+ // A hook breakdown whose instance has NO request counters is just as
747
+ // unprintable as a fallback: the categories are measured, but the total this
748
+ // instance knows is 0 (it never completed a step - e.g. a mirror that only
749
+ // saw the compaction hook). Such an entry also takes the state.json path, so
750
+ // the header and the categories come from ONE measurement instead of a
751
+ // `ctx=0` header sitting above non-zero categories.
752
+ const haveLocalCounters =
753
+ (typeof inputTokens === "number" && inputTokens > 0) ||
754
+ (typeof ctxTokens === "number" && ctxTokens > 0);
755
+ if (
756
+ !bd ||
757
+ bd.source === "client-context" ||
758
+ (isHookSource(bd.source) && !haveLocalCounters)
759
+ ) {
745
760
  const entry = isSafeSessionKey(sessionID)
746
761
  ? readStateSnapshot()[sessionID]
747
762
  : null;
@@ -750,13 +765,33 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
750
765
  snapInputTokens = safeSaneOrNull(entry.input);
751
766
  snapCtxTokens = safeSaneOrNull(entry.ctx);
752
767
  snapAt = typeof entry.updatedAt === "string" ? entry.updatedAt : null;
768
+ borrowedSnapshot = true;
753
769
  }
754
770
  }
755
771
  const source = bd?.source || "none";
756
772
  const rt = typeof reasoningTokens === "number" ? reasoningTokens : 0;
757
773
  const effInput = snapInputTokens != null ? snapInputTokens : inputTokens;
758
774
  const effCtx = snapCtxTokens != null ? snapCtxTokens : ctxTokens;
759
- const total = typeof effCtx === "number" && effCtx > 0 ? effCtx : effInput;
775
+ // Only a POSITIVE counter is knowledge. A zero is exactly what the broken
776
+ // case looks like (a state entry written by the trailing-event fallback of a
777
+ // mirror instance carries `ctx: 0, input: 0`), and treating it as "known"
778
+ // would reprint the very `ctx=0` this path exists to avoid.
779
+ const haveSnapCounters =
780
+ (typeof snapInputTokens === "number" && snapInputTokens > 0) ||
781
+ (typeof snapCtxTokens === "number" && snapCtxTokens > 0);
782
+ // Still unknown (no counters here AND no usable hook measurement on disk):
783
+ // print "?" rather than a fabricated 0 that contradicts the categories below.
784
+ const countersKnown = haveLocalCounters || haveSnapCounters;
785
+ // `other = input - sum` needs the input itself; a ctx-only snapshot (the
786
+ // compaction hook knows ctx but a mirror never saw the request) cannot
787
+ // produce an honest residual.
788
+ const inputKnown =
789
+ countersKnown && typeof effInput === "number" && effInput > 0;
790
+ const total = !countersKnown
791
+ ? null
792
+ : typeof effCtx === "number" && effCtx > 0
793
+ ? effCtx
794
+ : effInput;
760
795
  const denom =
761
796
  usableInfo != null
762
797
  ? usableInfo.usable
@@ -764,13 +799,19 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
764
799
  ? limit
765
800
  : null;
766
801
  const pct =
767
- denom != null ? ` (${((total / denom) * 100).toFixed(1)}%)` : "";
802
+ denom != null && total != null
803
+ ? ` (${((total / denom) * 100).toFixed(1)}%)`
804
+ : "";
768
805
  // The reader must be able to tell a fresh local measurement from a borrowed
769
- // snapshot (and from how old it is).
770
- const snapNote = snapAt ? ` snapshot=${snapAt}` : "";
806
+ // snapshot (and from how old it is). A snapshot without a timestamp still
807
+ // must not look local.
808
+ const snapNote = borrowedSnapshot
809
+ ? ` snapshot=${snapAt || "unknown"}`
810
+ : "";
771
811
 
812
+ const shownTotal = total != null ? fmt(total) : "?";
772
813
  lines.push(
773
- `[${ts}] session=${sessionID} model=${modelID} ctx=${fmt(total)}/${denom != null ? fmt(denom) : "?"}${pct} count=${fmt(total)} usable=${usableInfo != null ? fmt(usableInfo.usable) : "?"} limit=${limit != null ? fmt(limit) : "?"} maxOutput=${usableInfo != null ? fmt(usableInfo.maxOutput) : "?"} reserved=${usableInfo != null ? fmt(usableInfo.reserved) : "?"} input=${fmt(effInput)}${snapNote}`,
814
+ `[${ts}] session=${sessionID} model=${modelID} ctx=${shownTotal}/${denom != null ? fmt(denom) : "?"}${pct} count=${shownTotal} usable=${usableInfo != null ? fmt(usableInfo.usable) : "?"} limit=${limit != null ? fmt(limit) : "?"} maxOutput=${usableInfo != null ? fmt(usableInfo.maxOutput) : "?"} reserved=${usableInfo != null ? fmt(usableInfo.reserved) : "?"} input=${inputKnown ? fmt(effInput) : "?"}${snapNote}`,
774
815
  );
775
816
 
776
817
  // Residual = input minus every measured category (shared normaliser; `bd`
@@ -778,10 +819,11 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
778
819
  // breakdown taken from state.json carries the residual the HOOK computed
779
820
  // against the input IT saw; this instance's `inputTokens` can differ (a mirror
780
821
  // that only saw the trailing event has 0), so that value is used instead.
781
- const other =
782
- typeof bd?.otherChars === "number"
822
+ const other = inputKnown
823
+ ? typeof bd?.otherChars === "number"
783
824
  ? bd.otherChars
784
- : breakdownCategories(bd, effInput).other;
825
+ : breakdownCategories(bd, effInput).other
826
+ : null;
785
827
 
786
828
  if (bd) {
787
829
  lines.push(` user ${fmt(bd.userChars || 0)} (est tokens)`);
@@ -811,7 +853,11 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
811
853
  lines.push(` system n/a (hook did not run)`);
812
854
  lines.push(` tool schemas n/a`);
813
855
  }
814
- lines.push(` other ${fmt(other)} (= input - sum of estimates)`);
856
+ lines.push(
857
+ other != null
858
+ ? ` other ${fmt(other)} (= input - sum of estimates)`
859
+ : ` other n/a (input unknown on this instance)`,
860
+ );
815
861
  lines.push(` source ${source}`);
816
862
 
817
863
  // Provider-reported prompt cache (EXACT, not an estimate; a subset of input —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-context-indicator",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Real-time context-window usage for OpenCode: per-category token breakdown, /context-diff, a zero-LLM /cx panel with per-session cost and a searchable family-session picker, a TUI sidebar, and an experimental Desktop panel. Dual V1 + V2 plugin entry.",
5
5
  "type": "module",
6
6
  "license": "MIT",