opencode-context-indicator 1.7.1 → 1.8.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.
@@ -1,76 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- // lib/tui-sidebar.tsx — the `sidebar.content` slot component. Moved verbatim
3
- // from tui.tsx when the TUI entry was split: the host's Solid transform
4
- // applies to EVERY plugin file outside node_modules (solid-plugin.js
5
- // sourceFilter in @opentui/solid), so JSX in imported files is compiled the
6
- // same way as in the entry.
7
-
8
- import { usePlugin } from "@opencode/plugin/tui"
9
- import { Show } from "solid-js"
10
- import type { StateEntry } from "./tui-data.js"
11
- import { fmt, num, updatedAt } from "./tui-data.js"
12
- import { token } from "./tui-theme.js"
13
-
14
- // Sidebar category-label column width (monospace), used by the `line()` helper.
15
- const LABEL_WIDTH = 12
16
-
17
- export function Sidebar(props: { entry: () => StateEntry | null }) {
18
- const context = usePlugin()
19
- const base = () => token(context?.theme?.text?.base, "white")
20
- const muted = () => token(context?.theme?.text?.muted, base())
21
-
22
- const e = () => props.entry()
23
-
24
- const denom = (): number | null => {
25
- const x = e()
26
- const d = x?.usable ?? x?.limit
27
- return typeof d === "number" && d > 0 ? d : null
28
- }
29
- const pct = (v: number): string => {
30
- const d = denom()
31
- return d ? `${Math.round((v / d) * 100)}%` : ""
32
- }
33
- const line = (label: string, v: unknown): string => {
34
- const n = num(v)
35
- const l = (label + " ".repeat(LABEL_WIDTH)).slice(0, LABEL_WIDTH)
36
- const p = pct(n)
37
- return `${l}${fmt(n).padStart(7)}${p ? ` ${p.padStart(4)}` : ""}`
38
- }
39
- const header = (): string => {
40
- const x = e()
41
- if (!x) return ""
42
- const d = denom()
43
- return `${x.model ?? "?"} · ${fmt(num(x.ctx))}${
44
- d ? ` / ${fmt(d)}` : ""
45
- }${d ? ` (${pct(num(x.ctx))})` : ""}`
46
- }
47
-
48
- return (
49
- <box flexDirection="column">
50
- <text fg={base()}>{"Context"}</text>
51
- <Show when={e()} fallback={<text fg={muted()}>{" no data yet"}</text>}>
52
- <text fg={base()}>{header()}</text>
53
- <text fg={muted()}>{line("user", e()?.categories?.user)}</text>
54
- <text fg={muted()}>{line("assistant", e()?.categories?.assistant)}</text>
55
- <text fg={muted()}>{line("reasoning", e()?.categories?.reasoning)}</text>
56
- <text fg={muted()}>{line("tool args", e()?.categories?.toolArgs)}</text>
57
- <text fg={muted()}>{line("tool out", e()?.categories?.toolOut)}</text>
58
- <text fg={muted()}>
59
- {e()?.categories?.system == null
60
- ? `${"system".padEnd(LABEL_WIDTH)} n/a`
61
- : line("system", e()?.categories?.system)}
62
- </text>
63
- <text fg={muted()}>
64
- {e()?.categories?.toolSchemas == null
65
- ? `${"tool schemas".padEnd(LABEL_WIDTH)} n/a`
66
- : line("tool schemas", e()?.categories?.toolSchemas)}
67
- </text>
68
- <text fg={muted()}>{line("other", e()?.categories?.other)}</text>
69
- <Show when={e()?.compactStale === true}>
70
- <text fg={muted()}>{" stale — compacted"}</text>
71
- </Show>
72
- <text fg={muted()}>{`updated ${updatedAt(e()?.updatedAt)}`}</text>
73
- </Show>
74
- </box>
75
- )
76
- }
package/tui.tsx DELETED
@@ -1,206 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- /**
3
- * tui.tsx — optional OpenCode TUI sidebar for `opencode-context-indicator`.
4
- *
5
- * The import-source pragma on the first line above is REQUIRED: without it the
6
- * opencode TUI loader (Bun) transforms JSX against the React runtime and emits
7
- * `import ... from "react/jsx-runtime"`, which the TUI runtime does not provide,
8
- * so loading the plugin from the npm cache throws `Cannot find package 'react'`
9
- * (react only ever resolved from a developer's local node_modules). The pragma
10
- * must stay the very first line, before any import, and no other pragma-like
11
- * token may appear in this file. See the OpenTUI Solid integration guide.
12
- *
13
- * This file is the ENTRY + setup wiring only. The components and the data layer
14
- * live in lib/ (all of them are transformed by the host exactly like this file,
15
- * because the Solid transform's sourceFilter covers every plugin file outside
16
- * node_modules — see @opentui/solid/scripts/solid-plugin.js):
17
- * - lib/tui-sidebar.tsx the `sidebar.content` slot component
18
- * - lib/tui-panel.tsx the `session.panel` component (BreakdownPanel)
19
- * - lib/tui-data.js state.json bridge, family walk, formatting helpers
20
- * (+ lib/tui-data.d.ts for the shared types)
21
- * - lib/tui-theme.js theme-token helper
22
- * - lib/contributors.js measured-contributor computation + redacted export
23
- *
24
- * Exposed as the `./tui` package entry ("beside the main plugin for automatic
25
- * loading"). OpenCode loads it only in the terminal TUI; it is NOT loaded in
26
- * the Desktop app (Desktop exposes no TUI slot API). It renders the
27
- * per-category context breakdown produced by the server plugin (index.js) into
28
- * the session sidebar via `append: "sidebar.content"` and into a full session
29
- * panel (`append: "session.panel"`) opened by the `cx` slash command / keymap
30
- * layer registered from an `append: "app"` slot.
31
- *
32
- * Data bridge: the server plugin rewrites
33
- * %TEMP%/opencode-context-indicator-state.json
34
- * (see writeStateFile() in index.js) on every breakdown update. This module
35
- * ONLY reads that file and re-polls it once per second; a missing or corrupt
36
- * file renders "no data yet". The main plugin never imports the TUI entry, so
37
- * the absence of the OpenTUI peers cannot break the server/V1 path.
38
- *
39
- * Runtime requirements: @opentui/core and solid-js are optional peers resolved
40
- * by OpenCode at runtime. @opentui/solid is a pinned direct dependency (exact
41
- * 0.7.0, the version opencode 2.2.0 ships): OpenCode's TUI loader does not
42
- * expose a host instance of it, so for npm-installed plugins the JSX pragma
43
- * would otherwise fail to resolve `@opentui/solid/jsx-runtime` (upstream
44
- * opencode issue #33884). @opencode/plugin 2.2.0 declares OpenTUI 0.7.0 as its
45
- * peer, so a plugin left on 0.6.x no longer installs against that host.
46
- *
47
- * Graceful mode: when this file is loaded from an npm install (its own path is
48
- * inside a `node_modules` directory), OpenCode skips the host Solid transform
49
- * for `node_modules` paths, so a mounted slot renders once and then never
50
- * live-updates (anomalyco/opencode#33884). Rather than freeze a permanent
51
- * "no data yet" panel, the entry detects that case, logs a single hint and
52
- * registers no slot (neither the sidebar nor the session panel). Install via
53
- * `file://` from a path outside any `node_modules` for the live surfaces (see
54
- * README → "Live sidebar").
55
- */
56
- import { Plugin } from "@opencode/plugin/tui"
57
- import { createSignal, Show } from "solid-js"
58
- import { mkdirSync, writeFileSync } from "node:fs"
59
- import { tmpdir } from "node:os"
60
- import { join } from "node:path"
61
- import type { StateEntry } from "./lib/tui-data.js"
62
- import { isNodeModulesInstall, POLL_MS, readSnapshot } from "./lib/tui-data.js"
63
- import { Sidebar } from "./lib/tui-sidebar"
64
- import { BreakdownPanel } from "./lib/tui-panel"
65
-
66
- // Session-panel contribution name. Session-panel names are shared selection
67
- // values (not registered claims), so it MUST be plugin-prefixed to avoid
68
- // colliding with another plugin's panel.
69
- const PANEL_NAME = "context-indicator.breakdown"
70
-
71
- export default Plugin.define({
72
- id: "context-indicator.tui",
73
- setup(context) {
74
- // npm installs (path under node_modules) cannot get live slot updates on
75
- // this OpenCode version. Stay inert and log a single hint instead of
76
- // registering slots that would freeze on "no data yet".
77
- if (isNodeModulesInstall()) {
78
- console.log(
79
- "[context-indicator] TUI sidebar and breakdown panel disabled: " +
80
- "npm-installed plugins cannot get live updates on this OpenCode " +
81
- "version (anomalyco/opencode#33884). Install via file:// (see README " +
82
- '→ "Live sidebar") for the live sidebar and panel. The /context slash ' +
83
- "commands still work everywhere.",
84
- )
85
- return
86
- }
87
-
88
- // Re-read the bridge file once per second and re-render reactively. ONE
89
- // ticker drives BOTH surfaces, and the snapshot is cached per tick: every
90
- // consumer (sidebar, panel data, messages) shares a single readFileSync.
91
- const [tick, setTick] = createSignal(0)
92
- let currentSession: string | undefined
93
- let snapCache: Record<string, StateEntry> | null = null
94
- let snapAt = -1
95
- const snapshot = (): Record<string, StateEntry> => {
96
- const t = tick() // track the ticker so reads re-run every poll
97
- if (snapCache === null || snapAt !== t) {
98
- snapCache = readSnapshot()
99
- snapAt = t
100
- }
101
- return snapCache
102
- }
103
- const entry = (): StateEntry | null =>
104
- snapshot()[currentSession ?? ""] ?? null
105
- const refresh = () => setTick((n) => n + 1)
106
- const timer = setInterval(() => setTick((n) => n + 1), POLL_MS)
107
-
108
- const unregisterSidebar = context.ui.slot({
109
- append: "sidebar.content",
110
- render: ({ sessionID }) => {
111
- currentSession = sessionID
112
- return <Sidebar entry={entry} />
113
- },
114
- })
115
-
116
- // A `session.panel` contribution is independently selectable: it renders
117
- // only while its name is the selected one, and the root session comes from
118
- // panel.sessionID.
119
- const unregisterPanel = context.ui.slot({
120
- append: "session.panel",
121
- render: (panel) => (
122
- <Show when={panel.name === PANEL_NAME}>
123
- <BreakdownPanel panel={panel} snapshot={snapshot} refresh={refresh} />
124
- </Show>
125
- ),
126
- })
127
-
128
- // The `app` slot renders nothing itself; it exists to register the global
129
- // keymap layer (slash command `cx` -> open the panel). The layer is owned by
130
- // this slot and disposed when the slot unregisters.
131
- const unregisterCommands = context.ui.slot({
132
- append: "app",
133
- render: () => {
134
- const keymap = context?.keymap
135
- if (keymap && typeof keymap.layer === "function") {
136
- keymap.layer(() => ({
137
- mode: "global",
138
- commands: [
139
- {
140
- id: "context-indicator.breakdown",
141
- title: "Context breakdown panel",
142
- slash: { name: "cx" },
143
- // Palette-visible AND directly bound: subagent session views
144
- // force the composer into its "subagents" tab, which pushes
145
- // keymap mode "composer" — the base-mode palette binding
146
- // (ctrl+p) is inert there and there is no prompt to type /cx.
147
- // A bind on this mode:"global" layer fires in every mode
148
- // (leader matches <leader>b "toggle sidebar"; f4 as a direct
149
- // single-chord fallback).
150
- palette: true,
151
- bind: "<leader>c,f4",
152
- run: () => {
153
- const panelApi = context?.ui?.panel
154
- if (panelApi && typeof panelApi.open === "function") {
155
- panelApi.open(PANEL_NAME)
156
- }
157
- },
158
- },
159
- ],
160
- }))
161
- }
162
- return null
163
- },
164
- })
165
-
166
- // One-time live-surface marker. The e2e Check A asserts this line from the
167
- // isolated instance's private log: it proves the ./tui entry not only
168
- // loaded but REGISTERED its surfaces — a host-transform failure of this
169
- // file would otherwise die silently between "loading plugin" and here.
170
- console.log(
171
- "[context-indicator] live TUI surfaces registered (sidebar + session.panel + keymap)",
172
- )
173
-
174
- // E2E instrumentation (only when test/e2e-tui.mjs sets the marker env):
175
- // same proof as the console line above, but via a FILE — the TUI captures
176
- // console output, so the log channel cannot carry it. Production runs
177
- // never set the env and never write the stamp.
178
- if (process.env.OPENCODE_CONTEXT_INDICATOR_TUI_MARKER) {
179
- try {
180
- mkdirSync(join(tmpdir(), "opencode-context-indicator"), { recursive: true })
181
- writeFileSync(
182
- join(tmpdir(), "opencode-context-indicator", "tui-live.stamp"),
183
- new Date().toISOString(),
184
- "utf8",
185
- )
186
- } catch {
187
- /* marker is best-effort test instrumentation */
188
- }
189
- }
190
-
191
- return () => {
192
- clearInterval(timer)
193
- for (const unregister of [
194
- unregisterSidebar,
195
- unregisterPanel,
196
- unregisterCommands,
197
- ]) {
198
- try {
199
- unregister?.()
200
- } catch {
201
- /* ignore */
202
- }
203
- }
204
- }
205
- },
206
- })