opencode-context-indicator 1.5.3 → 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 CHANGED
@@ -4,6 +4,44 @@ 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
+
7
45
  ## 1.5.3 — 2026-10-26
8
46
 
9
47
  - **Compaction ceiling aligned with opencode 2.1.4 (#51271).** `lib/limits.js`
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 v2.0.28**; the
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
@@ -302,8 +302,9 @@ notice if needed.
302
302
  are also seeded from the snapshot at startup, and the retry backs off
303
303
  (30 s → 5 min, capped). A known `usable`/`limit`/`agent` is never overwritten
304
304
  with `null` when another instance rewrites the snapshot.
305
- * All three commands append their payload to `context-breakdown.log` (final-summaries
306
- section), so it stays recoverable even if delivery fails.
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").
307
308
  * If `ctx.session.synthetic` throws, the command logs an actionable error and the
308
309
  payload is preserved in the log — there is no other delivery channel in this
309
310
  runtime (no `noReply`/`prompt` option exists).
@@ -405,16 +406,24 @@ and `$TMPDIR` (usually `/tmp`) elsewhere:
405
406
  | File | Purpose |
406
407
  | --- | --- |
407
408
  | `context-breakdown.log` | Human-readable live snapshot + bounded final summaries |
408
- | `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}`, `top` (top-5 heaviest single items, path-free), `previous` (previous distinct measurement for `/context-diff`), `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`. |
409
410
  | `context-events.log` | Raw event tap — **only** when `DEBUG_EVENTS` is flipped to `true` in the source (off by default) |
410
411
 
411
- The live `context-breakdown.log` snapshot is **rewritten** (never grows); final
412
- summaries (on `session.idle`) and compaction notes are **appended** — log writes
413
- use plain `appendFileSync`, not atomic replacement. Only the machine-readable
414
- `opencode-context-indicator-state.json` is written **atomically** (temp file +
415
- rename), so concurrent plugin instances cannot corrupt it. Cross-instance
416
- duplicate final summaries are prevented separately by an atomic claim marker
417
- (see `lib/dedup.js`).
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`).
418
427
 
419
428
  ---
420
429
 
@@ -617,7 +626,7 @@ caller.
617
626
  ### Running tests
618
627
 
619
628
  ```bash
620
- npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T28 unit checks
629
+ npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T29 unit checks
621
630
  npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
622
631
  npm run test:e2e:quick # E2E load-only, no LLM call
623
632
  npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
@@ -636,12 +645,12 @@ STEP 80 s, ~3 min total.
636
645
 
637
646
  | Step | What | Why |
638
647
  |---|---|---|
639
- | (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 |
640
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 |
641
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 |
642
- | (d) unit harness T1-T28 | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache, waste-detector, compaction-reserve, top-consumer and diff checks (74 total) | regressions in modelLimits, writeStateFile, hydrate, contributors, export |
643
- | (e) corporate-identifier scan | no forbidden identifiers in the published surface + test tree | compliance |
644
- | (f) non-English scan | no non-ASCII letters (`\p{L}` above U+007F) in any repo file | English-only source rule |
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 |
645
654
 
646
655
  **Isolated E2E — `test/e2e-tui.mjs`** (requires the OpenCode CLI)
647
656
 
@@ -655,7 +664,10 @@ The E2E is fully **non-invasive** to your running OpenCode Desktop:
655
664
  - `--standalone` starts a *private* server instead of the background service Desktop uses;
656
665
  - `OPENCODE_DB` points at a throwaway DB in `%TEMP%` (your sessions are untouched);
657
666
  - `OPENCODE_CONTEXT_INDICATOR_STATE_FILE` points at a throwaway state file in `%TEMP%`
658
- — the live `opencode-context-indicator-state.json` is never read or written;
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);
659
671
  - `OPENCODE_CONFIG_DIR` points at a throwaway global config written by the script
660
672
  (plugins: this checkout; `update: "disable"`) **for Check A only** — the load
661
673
  source is deterministic and fail-closed instead of following your live config;
Binary file
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
  };
package/lib/state.js CHANGED
@@ -52,7 +52,11 @@ const MAX_FINAL_SUMMARIES = 50; // bound the in-memory final-summary log
52
52
  const DEBUG_EVENTS = false;
53
53
  const EVENT_LOG_MAX_BYTES = 1024 * 1024; // ~1MB truncate threshold
54
54
 
55
- const LOG_FILE = join(tmpdir(), "context-breakdown.log");
55
+ // Human-readable live log. Overridable via env for test isolation (same
56
+ // contract as STATE_FILE); in production the env is never set.
57
+ const LOG_FILE =
58
+ process.env.OPENCODE_CONTEXT_INDICATOR_LOG_FILE ||
59
+ join(tmpdir(), "context-breakdown.log");
56
60
 
57
61
  // Machine-readable snapshot consumed by the optional TUI sidebar (./tui.tsx,
58
62
  // package entry "./tui"). Written next to each log update — same moment, same
@@ -149,12 +153,27 @@ function renderFile(liveLines) {
149
153
  // Atomically rewrite the whole log file (bounded by design). The temp+rename
150
154
  // makes concurrent writers from multiple plugin instances safe: a reader sees
151
155
  // either the previous or the new file, never a half-written one.
156
+ //
157
+ // The last rendered live snapshot is remembered: callers that only add a final
158
+ // summary / compaction note pass no live lines, and an empty snapshot section
159
+ // after every idle would make the log useless.
160
+ let lastLiveLines = [];
161
+
152
162
  function writeLog(liveLines) {
153
163
  try {
154
- const content = renderFile(liveLines);
164
+ if (Array.isArray(liveLines) && liveLines.length > 0) {
165
+ lastLiveLines = liveLines;
166
+ }
167
+ const content = renderFile(
168
+ Array.isArray(liveLines) && liveLines.length > 0 ? liveLines : lastLiveLines,
169
+ );
155
170
  // Random temp suffix + exclusive create ("wx"): a predictable name could be
156
171
  // pre-created as a symlink for us to follow (CWE-377). "wx" fails instead.
157
172
  const tmp = `${LOG_FILE}.${randomBytes(6).toString("hex")}.tmp`;
173
+ // Unlike writeStateFile there is no ensureRegularFile(LOG_FILE) guard: the
174
+ // destination is only ever replaced by rename(2), which swaps the directory
175
+ // entry and never follows a symlink, so a pre-planted link cannot redirect a
176
+ // write. The "wx" temp below already blocks the symlink-attack vector.
158
177
  // Create BEFORE the finally-guarded section (mirrors writeStateFile): on an
159
178
  // (astronomically unlikely) EEXIST collision with a peer's temp, THEIR file
160
179
  // must not be cleaned up by us — the write fails and is logged instead.
package/lib/tui-panel.tsx CHANGED
@@ -53,6 +53,74 @@ type PanelInput = {
53
53
  // (never once per poll) when it is unavailable and hide the whole section.
54
54
  let warnedMessagesUnavailable = false
55
55
 
56
+ // Cheap change detector for the live message list. The host hands back a fresh
57
+ // array on every poll, so identity cannot be used as a memo key, while walking
58
+ // the whole list (per-character estimates + JSON.stringify of tool args) once
59
+ // per 1000 ms tick is exactly the cost we want to avoid. Length + last message
60
+ // id + created stamp + last message payload size covers appends, streaming and
61
+ // tool completion; an in-place edit of an OLDER message lands one tick later,
62
+ // when the tail changes. Known blind spot by design: the size is a SUM, so a
63
+ // same-length substitution inside the last message is not detected.
64
+ function listSignature(list: unknown[]): string {
65
+ const n = list.length
66
+ if (n === 0) return "0"
67
+ const last: unknown = list[n - 1]
68
+ if (last == null || typeof last !== "object") return `${n}:?`
69
+ const rec = last as Record<string, unknown>
70
+ const id = typeof rec.id === "string" ? rec.id : ""
71
+ const time = rec.time
72
+ const created =
73
+ time != null && typeof time === "object"
74
+ ? String((time as Record<string, unknown>).created ?? "")
75
+ : ""
76
+ let size = typeof rec.text === "string" ? rec.text.length : 0
77
+ const parts: unknown[] = Array.isArray(rec.content) ? rec.content : []
78
+ for (const raw of parts) {
79
+ if (raw == null || typeof raw !== "object") continue
80
+ const part = raw as Record<string, unknown>
81
+ if (typeof part.text === "string") {
82
+ size += part.text.length
83
+ continue
84
+ }
85
+ const state = part.state
86
+ if (state == null || typeof state !== "object") continue
87
+ const st = state as Record<string, unknown>
88
+ if (typeof st.input === "string") size += st.input.length
89
+ else if (st.input != null) {
90
+ try {
91
+ size += JSON.stringify(st.input).length
92
+ } catch {
93
+ /* unstringifiable input contributes no size */
94
+ }
95
+ }
96
+ // Tool RESULTS matter as much as args: measureContributors reads
97
+ // state.content, and a call that finishes between two ticks changes nothing
98
+ // else on the message. Without this the measured section would freeze for
99
+ // the whole remainder of the turn.
100
+ const content = st.content
101
+ if (typeof content === "string") {
102
+ size += content.length
103
+ } else if (Array.isArray(content)) {
104
+ for (const rawItem of content) {
105
+ if (rawItem == null || typeof rawItem !== "object") continue
106
+ const item = rawItem as Record<string, unknown>
107
+ if (typeof item.text === "string") {
108
+ size += item.text.length
109
+ } else {
110
+ // Non-text parts (e.g. file refs with long/base64 URIs) are not
111
+ // measured by measureContributors, so their marker is enough: never
112
+ // JSON.stringify a potentially megabyte-sized reference per tick.
113
+ size += 1
114
+ }
115
+ }
116
+ }
117
+ // Status transitions (pending -> completed/error) are cheap and always
118
+ // accompany a visible change.
119
+ if (typeof st.status === "string") size += st.status.length
120
+ }
121
+ return `${n}:${id}:${created}:${size}`
122
+ }
123
+
56
124
  // Panel column layout (monospace), derived from the host-provided panel width
57
125
  // so cells never collide on narrow terminals. Gaps between columns keep a
58
126
  // truncated cell visually separated from its neighbour.
@@ -253,14 +321,38 @@ export function BreakdownPanel(props: {
253
321
  }
254
322
  })
255
323
 
324
+ // Memo for the measured contributors section: the signature/key pair keeps
325
+ // the expensive walk out of the per-tick path (see listSignature).
326
+ let contributorKey: string | null = null
327
+ let contributorValue: Contributor[] = []
328
+
256
329
  const contributors = createMemo<Contributor[]>(() => {
257
330
  const m = messages()
258
- if (!m.available) return []
331
+ if (!m.available) {
332
+ contributorKey = null
333
+ contributorValue = []
334
+ return contributorValue
335
+ }
336
+ // Re-measure ONLY when the message list actually changed. The host rebuilds
337
+ // the array (and `props.snapshot()` ticks) every second, so keying on array
338
+ // identity would re-walk every message — per-character token estimates plus
339
+ // JSON.stringify of tool args — once per tick on the render thread.
340
+ let key: string
341
+ try {
342
+ key = listSignature(m.list)
343
+ } catch {
344
+ key = ""
345
+ }
346
+ if (key && key === contributorKey) return contributorValue
259
347
  try {
260
- return measureContributors(m.list)
348
+ contributorValue = measureContributors(m.list)
349
+ contributorKey = key || contributorKey
261
350
  } catch {
262
- return []
351
+ // Transient failure: leave the key unset so the next tick retries.
352
+ contributorValue = []
353
+ contributorKey = null
263
354
  }
355
+ return contributorValue
264
356
  })
265
357
 
266
358
  // Redacted export (key `e`). No message text, no full session id, no tool
package/lib/v2.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import {
25
25
  BREAKDOWN_THROTTLE_MS,
26
26
  breakdownCache,
27
+ clearChildSubagentTracking,
27
28
  clearSessionTracking,
28
29
  ERROR_TOAST_DEDUP_MS,
29
30
  lastBdWrite,
@@ -833,6 +834,10 @@ async function handleV2Event(ctx, event) {
833
834
  // stale entry lingers, via the phantom filter — a deleted session has no
834
835
  // step). No dedicated in-memory index to clean up.
835
836
  clearSessionTracking(sid);
837
+ // Whatever this session was, its own descendants must go too: a deleted
838
+ // parent takes the whole subtree, and a deleted child takes its own
839
+ // sub-subagents (they would otherwise never be swept).
840
+ clearChildSubagentTracking(sid);
836
841
  return;
837
842
  }
838
843
 
@@ -922,7 +927,13 @@ async function handleV2Event(ctx, event) {
922
927
  } catch (err) {
923
928
  logErr("v2 final summary on idle failed", err);
924
929
  }
925
- clearSessionTracking(sid);
930
+ // Children keep their parent attribution + totals while the parent runs:
931
+ // otherwise the very next live line reports "subagents 0" right after a
932
+ // subagent finishes. A root idle sweeps the whole child set instead.
933
+ const parentID = sessionParentCache.get(sid);
934
+ const isSubagent = parentID != null;
935
+ clearSessionTracking(sid, false, isSubagent);
936
+ if (!isSubagent) clearChildSubagentTracking(sid);
926
937
  return;
927
938
  }
928
939
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-context-indicator",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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, and a TUI sidebar. Dual V1 + V2 plugin entry.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  "index.js",
13
13
  "lib/",
14
14
  "tui.tsx",
15
+ "docs/",
15
16
  "README.md",
16
17
  "CHANGELOG.md",
17
18
  "LICENSE"