opencode-context-indicator 1.5.0 → 1.5.2

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,42 @@ 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.2 — 2026-10-21
8
+
9
+ Maintenance release: no runtime behavior change. It aligns the dev toolchain
10
+ with opencode 2.1.2 and stops the CI drift gate from going red on patch
11
+ releases.
12
+
13
+ - Dev: `@opencode/plugin` devDependency pinned to `2.1.2`; `@opentui/solid`
14
+ and `@opentui/core` aligned with the 2.1.2 host (`0.6.6`).
15
+ - Tests: the e2e gate now terminates reliably on Windows. `process.exit()`
16
+ could deadlock inside Node's native exit after the node-pty/conpty teardown:
17
+ the run printed its verdict and then hung forever (which would also hang
18
+ `prepublishOnly`). It now sets `exitCode`, closes the conpty sockets on a
19
+ natural child exit, and keeps an unref'd watchdog as a safety net.
20
+ - Dev: `test/drift-watch.mjs` is now advisory on patch drift (warning, exit 0),
21
+ so the daily CI no longer goes red on every opencode patch. Breaking drift
22
+ still fails: a major bump, a minor bump of `@opencode/plugin`, a minor bump
23
+ for 0.x `@opentui/*`, or a structural host change (no `workspaces.catalog`
24
+ / no `@opentui` entries). `--strict` / `DRIFT_STRICT=1` fails on ANY drift
25
+ and is the release check; exit codes are set via `process.exitCode` so piped
26
+ CI logs are never truncated. README/CI unit-check counters updated to
27
+ T1-T25c / 62.
28
+
29
+ ## 1.5.1 — 2026-10-19
30
+
31
+ - **Compaction now handled by the real `compaction` hook.** opencode dispatches
32
+ a `compaction` hook at the start of a compaction request with the same
33
+ pre-compaction payload as the `context` hook. The plugin registers it and
34
+ persists the *true* pre-compaction breakdown at that moment — previously the
35
+ trailing `session.compacted` event re-persisted whatever the last model
36
+ request had left behind. The event is kept as a fallback (hosts/paths where
37
+ the hook does not fire), and a no-op compaction ("Nothing to compact yet")
38
+ writes no stale entry. Same `(stale — compacted)` UX, more accurate numbers.
39
+ - Dev: pinned the `@opencode/plugin` devDependency to `2.0.32`.
40
+ - Tests: T25 / T25a / T25b / T25c (compaction-hook snapshot, no-op guard,
41
+ event fallback, cold-cache clobber guard) — 62 unit checks total.
42
+
7
43
  ## 1.5.0 — 2026-10-16
8
44
 
9
45
  Highlights: two new context-health signals — the **provider-reported prompt
package/README.md CHANGED
@@ -499,13 +499,13 @@ the sidebar instead.
499
499
  gracefully (the breakdown falls back to throttled `session.messages` fetches
500
500
  and toasts keep working).
501
501
  * **OpenCode ≥ 2.0.16** for the V2 path (built and verified against
502
- **2.0.26**).
502
+ **2.1.2**).
503
503
  * **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
504
504
  ships with OpenCode. `@opentui/core` and `solid-js` are **optional** peer
505
505
  dependencies resolved by OpenCode at runtime; the main plugin installs and runs
506
506
  fine without them (the sidebar simply is not available). `@opentui/solid` is
507
- instead shipped as a **pinned direct dependency** (exact `0.5.17`, matching
508
- the OpenTUI stack OpenCode 2.0.26 ships): OpenCode's
507
+ instead shipped as a **pinned direct dependency** (exact `0.6.6`, matching
508
+ the OpenTUI stack OpenCode 2.1.2 ships): OpenCode's
509
509
  TUI loader does not expose a host instance of it, so for npm-installed plugins
510
510
  the JSX pragma would otherwise fail to resolve `@opentui/solid/jsx-runtime`
511
511
  (upstream: opencode issue #33884 — `node_modules` plugins are excluded from the
@@ -529,9 +529,10 @@ V2 path). It does not register configurable keys of its own.
529
529
 
530
530
  The package default export is a dual plugin:
531
531
 
532
- * `setup(ctx)` — V2 entry (OpenCode ≥ 2.0.16). Registers a `session.hook("context", …)`
533
- to capture the final messages / system prompt / tool schemas right before each
534
- model request, subscribes to the event stream (`session.created`,
532
+ * `setup(ctx)` — V2 entry (OpenCode ≥ 2.0.16). Registers `session.hook("context", …)`
533
+ and `session.hook("compaction", …)` to capture the final messages / system
534
+ prompt / tool schemas right before each model request (and each compaction
535
+ request), subscribes to the event stream (`session.created`,
535
536
  `session.step.ended`, `session.idle`, `session.compacted`, failure events),
536
537
  records limits from the model registry and registers the
537
538
  `/context` + `/context-breakdown` slash commands (via
@@ -549,12 +550,15 @@ duplicate final summaries are suppressed with an atomic claim marker
549
550
  (`lib/dedup.js`). Every file / estimate / subagent path is fault-tolerant: a
550
551
  failure is logged and never breaks the main event or toast path.
551
552
 
552
- **Compaction staleness.** OpenCode's compaction runs a separate flow that fires
553
- no `context` hook, so the numbers a `session.compacted` write re-persists are
554
- the *pre-compaction* ones. The plugin flags such an entry
555
- (`compactStale` in `state.json`) and every surface shows `(stale — compacted)`
556
- until the next model request refreshes the breakdown — i.e. the data is exactly
557
- as fresh as the last model request in the session.
553
+ **Compaction staleness.** OpenCode dispatches a dedicated `compaction` hook at
554
+ the start of a compaction request, carrying the same pre-compaction payload as
555
+ the `context` hook. The plugin registers it and persists the *true*
556
+ pre-compaction breakdown at that moment, flagging the entry stale
557
+ (`compactStale` in `state.json`) so every surface shows `(stale — compacted)`
558
+ until the next model request refreshes it. The trailing `session.compacted`
559
+ event is kept as a fallback (for hosts or paths where the hook does not fire);
560
+ a no-op compaction ("Nothing to compact yet") writes no stale entry. The data
561
+ is always exactly as fresh as the last model request in the session.
558
562
 
559
563
  This plugin does not touch `opencode-token-monitor`
560
564
  (`token_stats` / `token_history` / `token_export` keep working unchanged).
@@ -574,10 +578,11 @@ caller.
574
578
  ### Running tests
575
579
 
576
580
  ```bash
577
- npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1–T24 unit checks
581
+ npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T25c unit checks
578
582
  npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
579
583
  npm run test:e2e:quick # E2E load-only, no LLM call
580
- npm run test:drift # OpenTUI pin vs the latest RELEASED opencode (also in CI)
584
+ npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
585
+ npm run test:drift:strict # release check: fails on ANY host drift
581
586
  ```
582
587
 
583
588
  `prepublishOnly` runs **both** gates automatically on `npm publish`
@@ -595,7 +600,7 @@ STEP 80 s, ~3 min total.
595
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 |
596
601
  | (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 |
597
602
  | (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 |
598
- | (d) unit harness T1–T24 | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache and waste-detector checks (58 total) | regressions in modelLimits, writeStateFile, hydrate, contributors, export |
603
+ | (d) unit harness T1-T25c | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache and waste-detector checks (62 total) | regressions in modelLimits, writeStateFile, hydrate, contributors, export |
599
604
  | (e) corporate-identifier scan | no forbidden identifiers in the published surface + test tree | compliance |
600
605
  | (f) non-English scan | no non-ASCII letters (`\p{L}` above U+007F) in any repo file | English-only source rule |
601
606
 
@@ -640,9 +645,15 @@ repo inactivity — a push or manual run re-enables them):
640
645
  `lib/*` imports);
641
646
  2. `test/prepublish.mjs` — the full fast gate (a)–(f), including the standalone
642
647
  esbuild transpile check (pragmas, no React);
643
- 3. `test/drift-watch.mjs` — the OpenTUI pin must match the OpenTUI catalog of the
644
- **latest released** opencode; red CI means "bump the pin before the next
645
- release".
648
+ 3. `test/drift-watch.mjs` — compares the OpenTUI pins and the
649
+ `@opencode/plugin` types devDependency with the **latest released** opencode.
650
+ It is **advisory by default**: patch drift prints a `::warning::` annotation
651
+ and exits 0, so the daily pipeline does not go red on every host patch. Only
652
+ a **breaking** drift fails: a major bump, a minor bump of
653
+ `@opencode/plugin` (its TUI type surface is not guaranteed additive), a
654
+ minor bump for 0.x `@opentui/*`, or a structural host change (no
655
+ `workspaces.catalog` / no `@opentui` entries). Before a release run
656
+ `npm run test:drift:strict` to fail on any drift.
646
657
 
647
658
  The Node-pty E2E is not run in CI (requires the OpenCode binary) and is skipped
648
659
  gracefully when the CLI is absent (`[SKIP] opencode CLI not found`).
package/index.js CHANGED
@@ -150,7 +150,7 @@
150
150
  */
151
151
 
152
152
  import { makeV1 } from "./lib/v1.js";
153
- import { handleV2Event, onV2Context, estimateBreakdownFromContext, estimateBreakdownFromClientMessages } from "./lib/v2.js";
153
+ import { handleV2Event, onV2Context, onV2Compaction, estimateBreakdownFromContext, estimateBreakdownFromClientMessages } from "./lib/v2.js";
154
154
  import { breakdownCategories, computeReclaim, contentFingerprint, estimateTokens, extractToolTarget, isSafeSessionKey, safeCell, safeReclaim, safeSaneOrNull, safeToolOutByTool, sumCategories, toolOutEntries } from "./lib/estimate.js";
155
155
  import {
156
156
  commandContextBreakdown,
@@ -170,7 +170,7 @@ import {
170
170
  modelLimits,
171
171
  reserveFor,
172
172
  } from "./lib/limits.js";
173
- import { lastKnownCacheRead, lastKnownCacheWrite, sessionAgentCache, sessionParentCache } from "./lib/cache.js";
173
+ import { lastKnownCacheRead, lastKnownCacheWrite, lastKnownCtxCache, lastKnownInputCache, lastKnownReasoningCache, sessionAgentCache, sessionParentCache } from "./lib/cache.js";
174
174
  import {
175
175
  logErr,
176
176
  readStateSnapshot,
@@ -204,6 +204,7 @@ export {
204
204
  estimateBreakdownFromContext,
205
205
  estimateBreakdownFromClientMessages,
206
206
  onV2Context,
207
+ onV2Compaction,
207
208
  safeToolOutByTool,
208
209
  toolOutEntries,
209
210
  sumCategories,
@@ -218,6 +219,10 @@ export {
218
219
  safeSaneOrNull,
219
220
  lastKnownCacheRead,
220
221
  lastKnownCacheWrite,
222
+ lastKnownCtxCache,
223
+ lastKnownInputCache,
224
+ lastKnownReasoningCache,
225
+ handleV2Event,
221
226
  };
222
227
 
223
228
  // ---------------------------------------------------------------------------
@@ -238,6 +243,19 @@ export default {
238
243
  logErr("v2 context hook registration failed", err);
239
244
  }
240
245
 
246
+ // The compaction request carries the same pre-compaction context payload as
247
+ // the context hook. Registering it lets us persist the TRUE pre-compaction
248
+ // breakdown (see onV2Compaction); the `session.compacted` event then only
249
+ // acts as a fallback. A registration failure is non-fatal — the event path
250
+ // still marks the entry stale.
251
+ try {
252
+ registrations.push(
253
+ await ctx.session.hook("compaction", (context) => onV2Compaction(context)),
254
+ );
255
+ } catch (err) {
256
+ logErr("v2 compaction hook registration failed", err);
257
+ }
258
+
241
259
  // The model registry is captured asynchronously through ensureModelLimits
242
260
  // (ctx.model.list(), rank 3) — the authoritative source that carries the
243
261
  // user's merged config overrides. ctx.model.transform fires synchronously
package/lib/cache.js CHANGED
@@ -69,6 +69,13 @@ const lastErrorToast = new Map();
69
69
  // sessionID -> timestamp — dedup for session.compacted toasts
70
70
  const lastCompactedToast = new Map();
71
71
 
72
+ // sessionID -> timestamp — when the `compaction` HOOK last fired for a session.
73
+ // The hook is dispatched at the START of a compaction request (before the model
74
+ // summarises), carrying the exact pre-compaction context, so it lets us persist
75
+ // the TRUE pre-compaction breakdown. It also tells the later `session.compacted`
76
+ // EVENT that the hook already marked the entry (the event stays a fallback).
77
+ const lastCompactionHookAt = new Map();
78
+
72
79
  // sessionID -> parentID (null = main session) — [main]/[sub] attribution cache
73
80
  const sessionParentCache = new Map();
74
81
 
@@ -144,6 +151,8 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
144
151
  v2ContextFetchTimes.delete(sessionID);
145
152
  // Per-session tool-schema TTL cache (see estimateBreakdownFromContext).
146
153
  toolSchemasBySession.delete(sessionID);
154
+ // Compaction-hook marker (re-armed by the next compaction on this session).
155
+ lastCompactionHookAt.delete(sessionID);
147
156
  }
148
157
 
149
158
  export {
@@ -168,6 +177,7 @@ export {
168
177
  lastBdWrite,
169
178
  lastErrorToast,
170
179
  lastCompactedToast,
180
+ lastCompactionHookAt,
171
181
  sessionParentCache,
172
182
  sessionTotals,
173
183
  sessionAgentCache,
package/lib/v2.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  lastBdWrite,
30
30
  lastKnownCtx,
31
31
  lastKnownCtxCache,
32
+ lastCompactionHookAt,
32
33
  lastKnownCacheRead,
33
34
  lastKnownCacheReadCache,
34
35
  lastKnownCacheWrite,
@@ -68,6 +69,11 @@ const V2_CONTEXT_FETCH_THROTTLE_MS = 5000; // min gap between fallback fetches
68
69
  // event handlers): cap each at 3s so a slow/hung client cannot pin resources.
69
70
  const V2_FALLBACK_TIMEOUT_MS = 3000;
70
71
 
72
+ // Window within which a `session.compacted` EVENT is considered already handled
73
+ // by the preceding `compaction` HOOK: if the hook marked the entry recently, the
74
+ // event only logs + clears tracking instead of re-persisting the snapshot.
75
+ const COMPACTION_HOOK_FALLBACK_MS = 60000;
76
+
71
77
  // Estimate the per-category breakdown directly from a V2 SessionContext.
72
78
  // `context.messages` are @opencode/ai Message objects:
73
79
  // { role, content: [ {type:"text",text} | {type:"media"} |
@@ -329,6 +335,93 @@ function onV2Context(context) {
329
335
  }
330
336
  }
331
337
 
338
+ // V2 compaction hook handler: opencode dispatches `compaction` at the START of a
339
+ // compaction request with the SAME pre-compaction payload as the context hook
340
+ // (system / messages / tools / model). We use it to persist the TRUE
341
+ // pre-compaction breakdown instead of relying on the `session.compacted` event
342
+ // to re-persist whatever a previous request left behind.
343
+ //
344
+ // The entry is marked stale BY CONSTRUCTION (the context is about to be
345
+ // compacted, so the numbers change once the summary lands and the next context
346
+ // hook fires). Guard: the hook also fires for a NO-OP compaction ("Nothing to
347
+ // compact yet") on a session with no completed request — there is no measured
348
+ // context to mark, so we only cache and skip the state write (no fake stale
349
+ // badge).
350
+ function onV2Compaction(context) {
351
+ try {
352
+ if (!context || typeof context !== "object") return;
353
+ const sid = context.sessionID;
354
+ if (!sid) return;
355
+
356
+ const parts = estimateBreakdownFromContext(context, sid);
357
+ breakdownCache.set(sid, {
358
+ ...parts,
359
+ capturedAt: Date.now(),
360
+ source: "compaction-hook",
361
+ });
362
+ pruneMap(breakdownCache, MAX_TRACKED_SESSIONS);
363
+
364
+ const model = context.model;
365
+ if (model && typeof model === "object") {
366
+ if (typeof model.id === "string") lastKnownModelCache.set(sid, model.id);
367
+ if (typeof model.providerID === "string") {
368
+ lastKnownProviderCache.set(sid, model.providerID);
369
+ }
370
+ pruneMap(lastKnownModelCache, MAX_TRACKED_SESSIONS);
371
+ pruneMap(lastKnownProviderCache, MAX_TRACKED_SESSIONS);
372
+ }
373
+
374
+ // Arm the fallback guard for the trailing `session.compacted` event.
375
+ lastCompactionHookAt.set(sid, Date.now());
376
+ pruneMap(lastCompactionHookAt, MAX_TRACKED_SESSIONS);
377
+
378
+ const ctxTokens = lastKnownCtx(sid);
379
+ if (ctxTokens > 0) {
380
+ const lines = buildLiveLinesV2(
381
+ sid,
382
+ lastKnownModel(sid),
383
+ lastKnownProvider(sid),
384
+ lastKnownInput(sid),
385
+ ctxTokens,
386
+ lastKnownReasoning(sid),
387
+ );
388
+ writeStateFile(
389
+ sid,
390
+ lastKnownModel(sid),
391
+ lastKnownProvider(sid),
392
+ lastKnownInput(sid),
393
+ ctxTokens,
394
+ lastKnownReasoning(sid),
395
+ {
396
+ compacted: true,
397
+ cacheRead: lastKnownCacheRead(sid),
398
+ cacheWrite: lastKnownCacheWrite(sid),
399
+ },
400
+ );
401
+ // Claim key derived from the PAYLOAD shape (message count + total
402
+ // estimated chars): identical across plugin instances for the SAME
403
+ // compaction (cross-instance dedup preserved) yet distinct per compaction
404
+ // — a session-only key would log just the first compaction on a session.
405
+ const messages = Array.isArray(context.messages) ? context.messages : [];
406
+ const sig =
407
+ `${messages.length}:` +
408
+ (parts.userChars +
409
+ parts.assistantChars +
410
+ parts.reasoningChars +
411
+ parts.toolArgsChars +
412
+ parts.toolOutChars +
413
+ (parts.systemChars || 0) +
414
+ (parts.toolSchemasChars || 0));
415
+ pushFinalSummary(`compaction-hook:${sid}:${sig}`, [
416
+ `[${new Date().toISOString()}] COMPACTION (hook) session=${sid} — pre-compaction snapshot captured`,
417
+ ...lines.slice(1),
418
+ ]);
419
+ }
420
+ } catch (err) {
421
+ logErr("v2 compaction hook failed", err);
422
+ }
423
+ }
424
+
332
425
  // ---------------------------------------------------------------------------
333
426
  // V2 fallback: derive the message-based categories from the session's persisted
334
427
  // message list when the context hook did NOT populate the cache for this
@@ -685,23 +778,30 @@ async function handleV2Event(ctx, event) {
685
778
  lastKnownCtx(sid),
686
779
  lastKnownReasoning(sid),
687
780
  );
688
- writeStateFile(
689
- sid,
690
- lastKnownModel(sid),
691
- lastKnownProvider(sid),
692
- lastKnownInput(sid),
693
- lastKnownCtx(sid),
694
- lastKnownReasoning(sid),
695
- // This write re-persists the PRE-compaction snapshot (the compaction
696
- // flow fires no context hook — see README): mark the entry stale so the
697
- // UI shows the numbers as pre-compaction until the next model request
698
- // refreshes them.
699
- {
700
- compacted: true,
701
- cacheRead: lastKnownCacheRead(sid),
702
- cacheWrite: lastKnownCacheWrite(sid),
703
- },
704
- );
781
+ // The `compaction` HOOK already captured + persisted the exact
782
+ // pre-compaction snapshot and marked the entry stale. This event is now a
783
+ // FALLBACK: it re-persists only when the hook did NOT fire recently (host
784
+ // gap, mirror instance, or a compaction path that bypassed the hook).
785
+ // Either way the write re-persists the PRE-compaction numbers and marks
786
+ // the entry stale, so the UI shows them as pre-compaction until the next
787
+ // model request refreshes them.
788
+ const hookAt = lastCompactionHookAt.get(sid) || 0;
789
+ const handledByHook = Date.now() - hookAt < COMPACTION_HOOK_FALLBACK_MS;
790
+ if (!handledByHook) {
791
+ writeStateFile(
792
+ sid,
793
+ lastKnownModel(sid),
794
+ lastKnownProvider(sid),
795
+ lastKnownInput(sid),
796
+ lastKnownCtx(sid),
797
+ lastKnownReasoning(sid),
798
+ {
799
+ compacted: true,
800
+ cacheRead: lastKnownCacheRead(sid),
801
+ cacheWrite: lastKnownCacheWrite(sid),
802
+ },
803
+ );
804
+ }
705
805
  pushFinalSummary(
706
806
  `compacted:${event.id ?? sid}`,
707
807
  [
@@ -886,6 +986,7 @@ async function handleV2Event(ctx, event) {
886
986
 
887
987
  export {
888
988
  onV2Context,
989
+ onV2Compaction,
889
990
  handleV2Event,
890
991
  estimateBreakdownFromContext,
891
992
  estimateBreakdownFromClientMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-context-indicator",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Real-time context-window usage for OpenCode: per-category token breakdown, 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",
@@ -61,11 +61,11 @@
61
61
  }
62
62
  },
63
63
  "dependencies": {
64
- "@opentui/solid": "0.5.17"
64
+ "@opentui/solid": "0.6.6"
65
65
  },
66
66
  "devDependencies": {
67
- "@opencode/plugin": "2.0.30",
68
- "@opentui/core": "0.5.17",
67
+ "@opencode/plugin": "2.1.2",
68
+ "@opentui/core": "0.6.6",
69
69
  "@types/node": "26.6.3",
70
70
  "esbuild": "^0.28.2",
71
71
  "node-pty": "^1.1.0",
@@ -76,7 +76,8 @@
76
76
  "test:prepublish": "node test/prepublish.mjs",
77
77
  "test:e2e": "node test/e2e-tui.mjs --full",
78
78
  "test:e2e:quick": "node test/e2e-tui.mjs --quick",
79
- "test:drift": "node test/drift-watch.mjs"
79
+ "test:drift": "node test/drift-watch.mjs",
80
+ "test:drift:strict": "node test/drift-watch.mjs --strict"
80
81
  },
81
82
  "prepublishOnly": "node test/prepublish.mjs && node test/e2e-tui.mjs --full"
82
83
  }
package/tui.tsx CHANGED
@@ -38,7 +38,7 @@
38
38
  *
39
39
  * Runtime requirements: @opentui/core and solid-js are optional peers resolved
40
40
  * by OpenCode at runtime. @opentui/solid is a pinned direct dependency (exact
41
- * 0.5.17): OpenCode's TUI loader does not expose a host instance of it, so for
41
+ * 0.6.6): OpenCode's TUI loader does not expose a host instance of it, so for
42
42
  * npm-installed plugins the JSX pragma would otherwise fail to resolve
43
43
  * `@opentui/solid/jsx-runtime` (upstream opencode issue #33884).
44
44
  *