opencode-context-indicator 1.2.2 → 1.3.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 ADDED
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ Notable user-facing changes per release. Dates are commit days (UTC).
4
+ The package also ships a dual V1/V2 plugin — see the README for the surface
5
+ matrix; everything below concerns the V2 path unless stated otherwise.
6
+
7
+ ## 1.3.0 — unreleased
8
+
9
+ Highlights: per-session cost in `/cx` (family-aggregated for the main session),
10
+ a searchable family-session picker, and `/cx` reachable from subagent session
11
+ views.
12
+
13
+ - `/cx` panel: new **cost** column (upstream #45417). The main session shows
14
+ the family-aggregated spend, each subagent its own; `-` when the host has no
15
+ cost for a session.
16
+ - New panel key `s` — a searchable picker over all family sessions
17
+ (title / model / cost / running marker). `Enter` navigates: root sessions
18
+ focus their tab, subagent sessions switch the route.
19
+ - `/cx` now works from **subagent session views**: those views have no prompt
20
+ input, so the command is also in the command palette and bound to
21
+ `<leader>c` (ctrl+x, then c) and `f4`. The panel family is rooted at the
22
+ topmost ancestor, so the whole family is shown, not just the child subtree.
23
+ - OpenTUI pinned `0.5.17`; `@opencode/plugin` types devDep pinned `2.0.26`
24
+ (drift-watch now tracks both). Tested against opencode **2.0.26**.
25
+
26
+ ## 1.2.2 — 2026-10-08
27
+
28
+ - TUI entry split for maintainability: `tui.tsx` down to ~195 lines; the
29
+ components and the data layer moved to `lib/` (`tui-sidebar.tsx`,
30
+ `tui-panel.tsx`, `tui-data.js` + `.d.ts`, `tui-theme.js`). No behavior
31
+ change. The pragma regression gate now covers every `lib/*.tsx`.
32
+ - E2E hardened: deterministic fail-closed plugin-load check (isolated global
33
+ config via `OPENCODE_CONFIG_DIR` + the isolated instance's own log file +
34
+ a live-surface stamp), cleanup of spawned CLI instances, WMI probe timeouts.
35
+
36
+ ## 1.2.1 — 2026-10-08
37
+
38
+ - The panel / percentage denominator now equals **opencode's compaction
39
+ ceiling** (v2.0.23+ `calculateCeiling`: window = `limit.input ||
40
+ limit.context`; reserve = max(10% of the window, 16k when the window is at
41
+ least 32k)) — percentages read as "% to compaction", not "% of the raw
42
+ window" (upstream #51271).
43
+ - `state.json`: new `reserve` field; the panel and `/context` show
44
+ `compact at X (N reserve)`.
45
+ - Unit harness extended to 34 checks (measured contributors, redacted-export
46
+ invariants, ceiling formula, hydrate seeding).
47
+ - OpenTUI pinned `0.5.16`.
48
+
49
+ ## 1.2.0 — 2026-10-04
50
+
51
+ - New: **zero-LLM `/cx` session panel** — a family table (role / model /
52
+ ctx% / updated) for the current session and all subagent descendants, plus a
53
+ `largest contributors (measured)` section computed from the in-process
54
+ message list (per tool call, with message clock times and bounded previews).
55
+ - Redacted export (`e`): no message text, no full session ids, no tool output
56
+ content — written under `%TEMP%/opencode-context-indicator/exports/`.
57
+ - Keys: `esc` closes, `r` refreshes, `e` exports.
58
+
59
+ ## 1.1.6 — 2026-10-04
60
+
61
+ - OpenTUI pinned `0.5.14` (opencode 2.0.21+), optional-peer floor aligned.
62
+ - `index.js` split into `lib/` modules (no behavior change).
63
+
64
+ ## 1.1.5 — 2026-10-02
65
+
66
+ - Security hardening: sanitized table metadata, validated `state.json`
67
+ (prototype-poisoning keys dropped), ownership-safe state lock, tmp hygiene.
68
+
69
+ ## 1.1.4 — 2026-10-02
70
+
71
+ - Removed a stray `opencode/process` import from the TUI entry; the prepublish
72
+ gate now rejects bare `opencode/*` specifiers (not bridged at runtime).
73
+
74
+ ## 1.1.3 — 2026-10-01
75
+
76
+ - Stable context denominator across plugin instances: a transient
77
+ `ctx.model.list()` miss falls back to the shared snapshot instead of
78
+ clobbering persisted windows.
79
+
80
+ ## 1.1.2 — 2026-10-01
81
+
82
+ - Graceful sidebar/panel disable on npm installs (upstream #33884: `node_modules`
83
+ plugins skip the host Solid transform, so slots would freeze) — a single hint
84
+ is logged instead; `@opentui/solid` is shipped pinned; `file://` install docs.
85
+
86
+ ## 1.1.1 — 2026-10-01
87
+
88
+ - Fixed `Cannot find package 'react'` when loaded from the npm cache: the
89
+ `@jsxImportSource @opentui/solid` pragma must be the file's first line.
90
+
91
+ ## 1.1.0 — 2026-10-01
92
+
93
+ - New: `/context` (compact summary) and `/context-breakdown` (full table, main
94
+ + all subagent sessions) slash commands.
95
+ - Stable per-model window limits; strict subagent discovery.
96
+
97
+ ## 1.0.0 — 2026-09-30
98
+
99
+ - Initial release: dual V1/V2 plugin, real-time context indicator, per-category
100
+ token breakdown (system / tool schemas / user / assistant / reasoning / tool
101
+ args / residual), TUI sidebar.
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  Real-time **context-window usage** indicator for [OpenCode](https://opencode.ai),
4
4
  with a **per-category token breakdown** and an optional **terminal-UI sidebar**.
5
5
 
6
+ User-facing changes per release: [CHANGELOG.md](./CHANGELOG.md).
7
+
6
8
  It tells you how full the current model's context window is, and *what* is
7
9
  filling it: system prompt, tool schemas, user/assistant text, reasoning, tool
8
10
  arguments and the residual "other" bucket.
@@ -10,7 +12,7 @@ arguments and the residual "other" bucket.
10
12
  The package ships a **dual V1 + V2 plugin** (one entry works in both OpenCode
11
13
  generations) plus an optional TUI module (`./tui` entry) that renders a **live
12
14
  sidebar** and a **zero-LLM `/cx` breakdown panel** with measured contributors
13
- and a redacted export. The V2 path is **tested against opencode v2.0.25**; the
15
+ and a redacted export. The V2 path is **tested against opencode v2.0.26**; the
14
16
  V1 path against 1.18.29+.
15
17
 
16
18
  > **V1 is feature-frozen.** It is kept working for compatibility (bug fixes
@@ -240,7 +242,20 @@ back to a plain notice if needed.
240
242
  `/cx` opens a full `session.panel` showing the breakdown for the current session
241
243
  **and its subagent descendants**, read from the same `state.json` bridge as the
242
244
  sidebar. Keys: `/cx` opens, `esc` closes, `r` refreshes (re-read the bridge),
243
- `e` exports a redacted snapshot.
245
+ `e` exports a redacted snapshot, `s` picks a family session.
246
+
247
+ Every row carries a **cost** column read from the host data layer: a main
248
+ session shows the **family-aggregated** spend, each subagent its own — no more
249
+ invisible subagent spend (upstream #45417). `s` opens a searchable picker over
250
+ all family sessions (title / model / cost / running marker); `Enter` navigates
251
+ there — root sessions focus their tab, subagent sessions switch the route.
252
+
253
+ Works from **subagent session views too**: those views have no prompt input
254
+ (the composer switches to its subagents tab), so `/cx` is also registered in
255
+ the command palette and bound to `<leader>c` / `f4` — both fire from any
256
+ session view (the palette itself is only reachable outside the composer).
257
+ Opened from a subagent view, the family is rooted at the topmost ancestor, so
258
+ the whole family is shown, not just the child subtree.
244
259
 
245
260
  **Zero LLM calls.** Unlike `/context` and `/context-breakdown` (which run a real
246
261
  agent turn so the model renders the table), the panel only reads local TUI data
@@ -437,13 +452,13 @@ the sidebar instead.
437
452
  gracefully (the breakdown falls back to throttled `session.messages` fetches
438
453
  and toasts keep working).
439
454
  * **OpenCode ≥ 2.0.16** for the V2 path (built and verified against
440
- **2.0.25**).
455
+ **2.0.26**).
441
456
  * **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
442
457
  ships with OpenCode. `@opentui/core` and `solid-js` are **optional** peer
443
458
  dependencies resolved by OpenCode at runtime; the main plugin installs and runs
444
459
  fine without them (the sidebar simply is not available). `@opentui/solid` is
445
- instead shipped as a **pinned direct dependency** (exact `0.5.16`, matching
446
- the OpenTUI stack OpenCode 2.0.25 ships): OpenCode's
460
+ instead shipped as a **pinned direct dependency** (exact `0.5.17`, matching
461
+ the OpenTUI stack OpenCode 2.0.26 ships): OpenCode's
447
462
  TUI loader does not expose a host instance of it, so for npm-installed plugins
448
463
  the JSX pragma would otherwise fail to resolve `@opentui/solid/jsx-runtime`
449
464
  (upstream: opencode issue #33884 — `node_modules` plugins are excluded from the
package/lib/tui-panel.tsx CHANGED
@@ -58,27 +58,71 @@ let warnedMessagesUnavailable = false
58
58
  // truncated cell visually separated from its neighbour.
59
59
  const GUTTER = 2
60
60
  const CTX_WIDTH = 12
61
+ const COST_WIDTH = 8
61
62
 
62
63
  function panelLayout(panelWidth: number): {
63
64
  roleW: number
64
65
  modelW: number
65
66
  ctxW: number
67
+ costW: number
66
68
  updW: number
67
69
  gutter: string
68
70
  } {
69
71
  const w = num(panelWidth) > 0 ? num(panelWidth) : 60
70
72
  const ctxW = CTX_WIDTH
71
- const updW = Math.max(5, Math.min(8, w - ctxW - 3 * GUTTER - 24))
72
- const roleW = Math.max(12, Math.min(20, Math.floor(w * 0.3)))
73
- const modelW = Math.max(10, w - roleW - ctxW - updW - 3 * GUTTER)
74
- return { roleW, modelW, ctxW, updW, gutter: " ".repeat(GUTTER) }
73
+ const costW = COST_WIDTH
74
+ const updW = Math.max(5, Math.min(8, w - ctxW - 4 * GUTTER - 24))
75
+ const roleW = Math.max(12, Math.min(20, Math.floor(w * 0.28)))
76
+ const modelW = Math.max(8, w - roleW - ctxW - costW - updW - 4 * GUTTER)
77
+ return { roleW, modelW, ctxW, costW, updW, gutter: " ".repeat(GUTTER) }
78
+ }
79
+
80
+ // Dollar formatting for the cost column: compact, always `$`-prefixed, `-`
81
+ // when the host data layer has no cost for the session.
82
+ function money(n: number | null): string {
83
+ if (n == null || !Number.isFinite(n)) return "-"
84
+ const a = Math.abs(n)
85
+ if (a >= 1000) return `$${(n / 1000).toFixed(1)}k`
86
+ if (a >= 100) return `$${n.toFixed(1)}`
87
+ return `$${n.toFixed(2)}`
88
+ }
89
+
90
+ // The topmost ancestor of the panel's session: `/cx` opened from a subagent
91
+ // session view must show the WHOLE family (rooted at the main session), not
92
+ // just the child subtree. Walks parentID upward over the snapshot; `seen`
93
+ // guards against parentID loops.
94
+ function resolveRoot(
95
+ snapshot: Record<string, StateEntry>,
96
+ sessionID: string,
97
+ ): string {
98
+ let current = sessionID
99
+ const seen = new Set<string>([current])
100
+ for (;;) {
101
+ const entry = Object.prototype.hasOwnProperty.call(snapshot, current)
102
+ ? snapshot[current]
103
+ : undefined
104
+ const parent = entry?.parentID
105
+ if (
106
+ !parent ||
107
+ typeof parent !== "string" ||
108
+ !Object.prototype.hasOwnProperty.call(snapshot, parent) ||
109
+ seen.has(parent)
110
+ ) {
111
+ break
112
+ }
113
+ seen.add(parent)
114
+ current = parent
115
+ }
116
+ return current
75
117
  }
76
118
 
77
119
  // Full session panel: header + one row per session in the family (root plus
78
120
  // subagent descendants). It reads the same state.json bridge as the sidebar via
79
- // the shared 1 s tick, so both surfaces refresh on ONE poll. `panel.sessionID`
80
- // selects the root; `cx` opens the panel (registered from the "app" slot in
81
- // tui.tsx).
121
+ // the shared 1 s tick, so both surfaces refresh on ONE poll. The family is
122
+ // rooted at the panel session's TOPMOST ancestor, so opening the panel from a
123
+ // subagent view shows the whole family. `cx` opens the panel (registered from
124
+ // the "app" slot in tui.tsx; also reachable from the command palette, since
125
+ // subagent views have no prompt input).
82
126
  export function BreakdownPanel(props: {
83
127
  panel: PanelInput
84
128
  snapshot: () => Record<string, StateEntry>
@@ -88,6 +132,32 @@ export function BreakdownPanel(props: {
88
132
  const base = () => token(context?.theme?.text?.base, "white")
89
133
  const muted = () => token(context?.theme?.text?.muted, base())
90
134
 
135
+ // The host session data layer, re-read per call: individual accessors
136
+ // (family/get/status/cost) are optional across host versions and every use
137
+ // site guards them separately.
138
+ const dataApiSession = () => context?.data?.session
139
+
140
+ // Set while the picker dialog is on screen: the panel-local `s` key must not
141
+ // re-trigger the picker (a printable key typed into the dialog search would
142
+ // otherwise stack dialogs — the guard is belt-and-braces for hosts where the
143
+ // panel keeps focus while a dialog is open).
144
+ let pickerOpen = false
145
+
146
+ // Per-session cost from the host data layer (read under the shared poll tick,
147
+ // so it refreshes reactively). For a ROOT session the host AGGREGATES the
148
+ // whole family; for a child it is that session's own cost — exactly the
149
+ // per-subagent spend visibility the community asked for (upstream #45417).
150
+ // Guarded: older hosts may not expose the accessor.
151
+ const costOf = (sessionID: string | undefined): number | null => {
152
+ if (!sessionID) return null
153
+ try {
154
+ const cost = context?.data?.session?.cost?.(sessionID)
155
+ return typeof cost === "number" && Number.isFinite(cost) ? cost : null
156
+ } catch {
157
+ return null
158
+ }
159
+ }
160
+
91
161
  // Panel-local keys. The layer is owned by this component and disposed with it.
92
162
  // mode "global" + an explicit focused check is the portable fallback when no
93
163
  // target renderable is available: the keys act only while the panel owns input.
@@ -125,17 +195,29 @@ export function BreakdownPanel(props: {
125
195
  if (props.panel.focused) exportRedacted()
126
196
  },
127
197
  },
198
+ {
199
+ id: "context-indicator.breakdown.pick",
200
+ title: "Pick a family session",
201
+ bind: "s",
202
+ enabled: () => props.panel.focused && !pickerOpen,
203
+ run: () => {
204
+ if (props.panel.focused && !pickerOpen) openPicker()
205
+ },
206
+ },
128
207
  ],
129
208
  }))
130
209
  }
131
210
 
132
211
  // One snapshot read per poll tick (props.snapshot tracks the shared tick), so
133
212
  // the memo recomputes together with the sidebar instead of polling separately.
213
+ // The family is rooted at the topmost ancestor (resolveRoot): from a subagent
214
+ // view the whole family is shown, not just the child subtree.
134
215
  const data = createMemo(() => {
135
216
  const snap = props.snapshot()
217
+ const rootID = resolveRoot(snap, props.panel.sessionID)
136
218
  return {
137
- root: snap[props.panel.sessionID],
138
- rows: collectFamily(snap, props.panel.sessionID),
219
+ root: snap[rootID],
220
+ rows: collectFamily(snap, rootID),
139
221
  }
140
222
  })
141
223
 
@@ -228,6 +310,165 @@ export function BreakdownPanel(props: {
228
310
  }
229
311
  }
230
312
 
313
+ // Navigate to a family session: root sessions focus their tab (tab-aware,
314
+ // opens one when needed); child sessions navigate the route directly (tabs
315
+ // are root-session scoped). Falls back with a toast when the host exposes
316
+ // neither mechanism.
317
+ function navigateTo(sessionID: string): void {
318
+ try {
319
+ const ui = context?.ui
320
+ // Child detection is three-sourced: the host session info, OUR snapshot
321
+ // (state.json parentID), and the host root() accessor — get() alone can
322
+ // miss a session that has not been synced into the client store yet, and
323
+ // tabs.focus() on a child ID would target the wrong (root) tab.
324
+ let isChild = false
325
+ try {
326
+ const info = context?.data?.session?.get?.(sessionID)
327
+ isChild = !!(
328
+ info &&
329
+ typeof info === "object" &&
330
+ (info as Record<string, unknown>).parentID
331
+ )
332
+ } catch {
333
+ /* other sources below */
334
+ }
335
+ if (!isChild) {
336
+ const snapParent = props.snapshot()[sessionID]?.parentID
337
+ if (typeof snapParent === "string" && snapParent.length > 0) isChild = true
338
+ }
339
+ if (!isChild && typeof context?.data?.session?.root === "function") {
340
+ try {
341
+ if (context.data.session.root(sessionID) !== sessionID) isChild = true
342
+ } catch {
343
+ /* treat as root */
344
+ }
345
+ }
346
+ if (!isChild && ui?.tabs && typeof ui.tabs.focus === "function") {
347
+ if (ui.tabs.focus(sessionID)) return
348
+ }
349
+ if (ui?.router && typeof ui.router.navigate === "function") {
350
+ ui.router.navigate({ type: "session", sessionID })
351
+ return
352
+ }
353
+ try {
354
+ context?.ui?.toast?.show({
355
+ message: "session navigation not supported by this host",
356
+ variant: "warning",
357
+ })
358
+ } catch {
359
+ /* toast is best-effort */
360
+ }
361
+ } catch (err) {
362
+ console.error("[context-indicator] session navigation failed:", err)
363
+ }
364
+ }
365
+
366
+ // Family-session picker (key `s`): a select dialog over the host data layer's
367
+ // family rooted at the TOPMOST ancestor (matches the table; authoritative —
368
+ // includes registered sessions that never served a step; falls back to our
369
+ // state.json walk), searchable by title/model, each option carrying its own
370
+ // cost and a running marker. Enter navigates.
371
+ function openPicker(): void {
372
+ if (pickerOpen) return
373
+ const dialog = context?.ui?.dialog
374
+ if (!dialog || typeof dialog.select !== "function") {
375
+ try {
376
+ context?.ui?.toast?.show({
377
+ message: "session picker requires opencode 2.0.23+",
378
+ variant: "warning",
379
+ })
380
+ } catch {
381
+ /* toast is best-effort */
382
+ }
383
+ return
384
+ }
385
+
386
+ const rootID = resolveRoot(props.snapshot(), props.panel.sessionID)
387
+ let ids: string[] = []
388
+ try {
389
+ const family = dataApiSession()?.family?.(rootID)
390
+ if (Array.isArray(family)) ids = family.filter((id) => typeof id === "string")
391
+ } catch {
392
+ ids = []
393
+ }
394
+ if (ids.length === 0) {
395
+ // Host family unavailable: fall back to our state.json family walk.
396
+ ids = collectFamily(props.snapshot(), rootID)
397
+ .map((e) => e.sessionID)
398
+ .filter((id): id is string => typeof id === "string" && id.length > 0)
399
+ }
400
+
401
+ const labels = new Map<string, string>()
402
+ const options = ids.map((id) => {
403
+ let title = "(untitled)"
404
+ let model = ""
405
+ let running = false
406
+ try {
407
+ const info = dataApiSession()?.get?.(id)
408
+ if (info && typeof info === "object") {
409
+ const raw = info as Record<string, unknown>
410
+ title = safeText(raw.title) || title
411
+ const m = raw.model as { modelID?: unknown } | string | undefined
412
+ model =
413
+ typeof m === "string"
414
+ ? safeText(m)
415
+ : m && typeof m.modelID === "string"
416
+ ? safeText(m.modelID)
417
+ : ""
418
+ running = dataApiSession()?.status?.(id) === "running"
419
+ }
420
+ } catch {
421
+ /* defensive: host data layers may lack get/status */
422
+ }
423
+ if (!model) {
424
+ const fromSnapshot = props.snapshot()[id]?.model
425
+ if (typeof fromSnapshot === "string" && fromSnapshot !== "unknown") model = safeText(fromSnapshot)
426
+ }
427
+ const parts = [model, title, money(costOf(id))]
428
+ if (running) parts.push("running")
429
+ const label = parts.filter((p) => p.length > 0).join(" · ")
430
+ labels.set(id, label)
431
+ return { title: label, value: id }
432
+ })
433
+
434
+ if (options.length <= 1) {
435
+ try {
436
+ context?.ui?.toast?.show({
437
+ message: "no other family sessions",
438
+ variant: "info",
439
+ })
440
+ } catch {
441
+ /* toast is best-effort */
442
+ }
443
+ return
444
+ }
445
+
446
+ pickerOpen = true
447
+ try {
448
+ void dialog
449
+ .select({
450
+ title: "Family sessions",
451
+ options,
452
+ search: (query, opts) => {
453
+ const q = query.trim().toLowerCase()
454
+ if (!q) return opts
455
+ return opts.filter((o) => (labels.get(o.value) ?? o.title).toLowerCase().includes(q))
456
+ },
457
+ })
458
+ .then((picked) => {
459
+ pickerOpen = false
460
+ if (typeof picked === "string" && picked.length > 0) navigateTo(picked)
461
+ })
462
+ .catch(() => {
463
+ pickerOpen = false
464
+ /* dialog settle races are host-managed; nothing to recover */
465
+ })
466
+ } catch (err) {
467
+ pickerOpen = false
468
+ console.error("[context-indicator] session picker failed:", err)
469
+ }
470
+ }
471
+
231
472
  const header = (): string => {
232
473
  const r = data().root
233
474
  if (!r) return "Context breakdown"
@@ -245,6 +486,7 @@ export function BreakdownPanel(props: {
245
486
  roleW: number
246
487
  modelW: number
247
488
  ctxW: number
489
+ costW: number
248
490
  updW: number
249
491
  gutter: string
250
492
  } => panelLayout(props.panel.width)
@@ -258,6 +500,8 @@ export function BreakdownPanel(props: {
258
500
  L.gutter +
259
501
  fit("ctx", L.ctxW) +
260
502
  L.gutter +
503
+ fit("cost", L.costW) +
504
+ L.gutter +
261
505
  "updated"
262
506
  )
263
507
  }
@@ -277,6 +521,8 @@ export function BreakdownPanel(props: {
277
521
  L.gutter +
278
522
  fit(ctx, L.ctxW) +
279
523
  L.gutter +
524
+ fit(money(costOf(e.sessionID)), L.costW) +
525
+ L.gutter +
280
526
  updatedAt(e.updatedAt)
281
527
  )
282
528
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-context-indicator",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Real-time OpenCode context-window usage indicator: per-category token breakdown written to a log file, plus an optional TUI sidebar. Dual V1 + V2 plugin entry.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "lib/",
14
14
  "tui.tsx",
15
15
  "README.md",
16
+ "CHANGELOG.md",
16
17
  "LICENSE"
17
18
  ],
18
19
  "engines": {
@@ -56,11 +57,11 @@
56
57
  }
57
58
  },
58
59
  "dependencies": {
59
- "@opentui/solid": "0.5.16"
60
+ "@opentui/solid": "0.5.17"
60
61
  },
61
62
  "devDependencies": {
62
- "@opencode/plugin": "^2.0.19",
63
- "@opentui/core": "0.5.16",
63
+ "@opencode/plugin": "2.0.26",
64
+ "@opentui/core": "0.5.17",
64
65
  "@types/node": "26.6.3",
65
66
  "esbuild": "^0.28.2",
66
67
  "node-pty": "^1.1.0",
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.16): OpenCode's TUI loader does not expose a host instance of it, so for
41
+ * 0.5.17): 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
  *
@@ -138,6 +138,15 @@ export default Plugin.define({
138
138
  id: "context-indicator.breakdown",
139
139
  title: "Context breakdown panel",
140
140
  slash: { name: "cx" },
141
+ // Palette-visible AND directly bound: subagent session views
142
+ // force the composer into its "subagents" tab, which pushes
143
+ // keymap mode "composer" — the base-mode palette binding
144
+ // (ctrl+p) is inert there and there is no prompt to type /cx.
145
+ // A bind on this mode:"global" layer fires in every mode
146
+ // (leader matches <leader>b "toggle sidebar"; f4 as a direct
147
+ // single-chord fallback).
148
+ palette: true,
149
+ bind: "<leader>c,f4",
141
150
  run: () => {
142
151
  const panelApi = context?.ui?.panel
143
152
  if (panelApi && typeof panelApi.open === "function") {