opencode-codeops 2.2.0 → 2.2.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
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to CodeOps are recorded here.
4
4
 
5
+ ## 2.2.1 — 2026-10-10
6
+
7
+ ### Fixes
8
+
9
+ - scripts: pin the changelog feature guard to the document, not one release
10
+ - tui: precompile the packaged TUI entry for npm installs
11
+
5
12
  ## 2.2.0 — 2026-10-10
6
13
 
7
14
  ### Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,13 +11,14 @@
11
11
  "exports": {
12
12
  ".": "./plugin/index.ts",
13
13
  "./server": "./plugin/index.ts",
14
- "./tui": "./plugin/tui.tsx"
14
+ "./tui": "./plugin/tui.js"
15
15
  },
16
16
  "bin": {
17
17
  "opencode-codeops": "bin/index.mjs"
18
18
  },
19
19
  "scripts": {
20
20
  "test": "node --test",
21
+ "build:tui": "node scripts/build-tui.mjs",
21
22
  "typecheck": "tsc --noEmit",
22
23
  "check:version": "node scripts/check-version.mjs",
23
24
  "verify": "npm run typecheck && npm run test && npm run check:version",
package/plugin/tui.js ADDED
@@ -0,0 +1,113 @@
1
+ // Generated from plugin/tui.tsx — do not edit; run `npm run build:tui` to regenerate.
2
+ import { insert as _$insert } from "@opentui/solid";
3
+ import { createElement as _$createElement } from "@opentui/solid";
4
+ import { memo as _$memo } from "@opentui/solid";
5
+ import { createComponent as _$createComponent } from "@opentui/solid";
6
+ /**
7
+ * The CodeOps TUI entry: the live run-progress view in the session sidebar.
8
+ *
9
+ * The view is deliberately honest: it shows only reported values from the
10
+ * server's run state, always with an as-of time, and it renders nothing when
11
+ * there is no run, when the host lacks the events API, or when every call
12
+ * fails. There are no timers — the text recomputes whenever the component
13
+ * re-renders — and no file access; all display and merge logic lives in the
14
+ * shared helper module so it can be tested under plain Node.
15
+ *
16
+ * @module tui
17
+ */
18
+
19
+ import { Plugin, usePlugin } from "@opencode/plugin/tui";
20
+ import { createEffect, createResource, createSignal, onCleanup, Show } from "solid-js";
21
+ import { acceptRunCleared, acceptRunUpdate, describeRun, mergeRunState } from "../bin/lib/codeops-progress.mjs";
22
+ import { CodeOpsRpc, requestProgress } from "../bin/lib/codeops-rpc.mjs";
23
+
24
+ /**
25
+ * The live run-progress view.
26
+ *
27
+ * Resolves the session's location once, subscribes to the `updated` and
28
+ * `cleared` events before requesting an initial snapshot, and merges both
29
+ * delivery paths monotonically. A clear records its server-stamped
30
+ * `clearedAt`; both merge paths ignore deliveries at or before it, so a late
31
+ * snapshot or update can never resurrect a cleared run. Every failure path is
32
+ * silent: a host without the events API still renders from the snapshot, and
33
+ * an invalid or missing snapshot renders nothing.
34
+ *
35
+ * @returns The three display lines, or nothing when no run is known.
36
+ */
37
+ function CodeOpsProgress() {
38
+ const context = usePlugin();
39
+ const location = context.location ?? context.data.location.default();
40
+ const directory = location?.directory ?? "";
41
+ const [run, setRun] = createSignal(null);
42
+ const [clearedAt, setClearedAt] = createSignal(0);
43
+
44
+ // Subscribe BEFORE the snapshot: events are live-only, so a snapshot
45
+ // fetched first could miss an update arriving in between. Guarded: a host
46
+ // without the events API still renders from the snapshot below.
47
+ const stops = [];
48
+ try {
49
+ const events = context.client.rpc(CodeOpsRpc).events;
50
+ const stopUpdated = events.on("updated", event => {
51
+ const snapshot = acceptRunUpdate(event, directory);
52
+ if (snapshot && snapshot.updatedAt > clearedAt()) {
53
+ setRun(current => mergeRunState(current, snapshot));
54
+ }
55
+ });
56
+ stops.push(stopUpdated);
57
+ const stopCleared = events.on("cleared", event => {
58
+ const cleared = acceptRunCleared(event, directory);
59
+ if (!cleared) return;
60
+ // Record the clear time for every accepted clear, before the identity
61
+ // gate: a clear that arrives before the initial snapshot must still
62
+ // suppress that snapshot, or a cleared run could reappear.
63
+ setClearedAt(cleared.clearedAt);
64
+ const current = run();
65
+ if (current && cleared.plan === current.plan && cleared.sessionID === current.sessionID) {
66
+ setRun(null);
67
+ }
68
+ });
69
+ stops.push(stopCleared);
70
+ } catch {
71
+ // Events unavailable: the snapshot-only view below still renders.
72
+ }
73
+ onCleanup(() => stops.forEach(stop => stop()));
74
+
75
+ // Initial snapshot; null or a failure means "nothing known". The clear
76
+ // guard applies here too: a snapshot at or before the last clear is ignored.
77
+ const [initial] = createResource(async () => requestProgress(context.client, {
78
+ location
79
+ }));
80
+ createEffect(() => {
81
+ const snapshot = initial();
82
+ if (snapshot && snapshot.updatedAt > clearedAt()) {
83
+ setRun(current => mergeRunState(current, snapshot));
84
+ }
85
+ });
86
+ return _$createComponent(Show, {
87
+ get when() {
88
+ return run();
89
+ },
90
+ children: snapshot => {
91
+ const view = () => describeRun(snapshot(), Date.now());
92
+ return _$memo(() => view().lines.map(line => (() => {
93
+ var _el$ = _$createElement("text");
94
+ _$insert(_el$, line);
95
+ return _el$;
96
+ })()));
97
+ }
98
+ });
99
+ }
100
+
101
+ /**
102
+ * The opencode-codeops TUI plugin: claims the sidebar content slot with the
103
+ * live run view. The host disposes the claim when the plugin unloads.
104
+ */
105
+ export default Plugin.define({
106
+ id: "opencode-codeops-tui",
107
+ setup(context) {
108
+ return context.ui.slot({
109
+ append: "sidebar.content",
110
+ render: () => _$createComponent(CodeOpsProgress, {})
111
+ });
112
+ }
113
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Build the packaged TUI entry.
3
+ *
4
+ * OpenCode transpiles plugin TSX with its OpenTUI renderer runtime only for
5
+ * files outside `node_modules`. A published package always installs into
6
+ * `node_modules`, so a source `.tsx` entry falls back to the default React
7
+ * JSX transform and fails to load ("Cannot find package 'react'"). Compiling
8
+ * `plugin/tui.tsx` with `@opentui/solid`'s own transform — the same pipeline
9
+ * the host applies to project-local plugins — produces a plain-JavaScript
10
+ * entry whose runtime imports (`@opentui/solid`, `solid-js`) the host
11
+ * rewrites to its bundled renderer.
12
+ *
13
+ * The generated artifact `plugin/tui.js` is committed and shipped as the
14
+ * `./tui` export; `scripts/tui-entry-build.spec.test.mjs` fails when it is
15
+ * stale, so the release verification blocks a stale entry.
16
+ *
17
+ * @module build-tui
18
+ */
19
+
20
+ import { readFileSync, writeFileSync } from "node:fs"
21
+ import { createRequire } from "node:module"
22
+ import { dirname, join } from "node:path"
23
+ import { fileURLToPath, pathToFileURL } from "node:url"
24
+
25
+ /** Repository root: this file lives in `<root>/scripts/`. */
26
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
27
+
28
+ /** Source of the TUI entry, authored as TSX. */
29
+ const SOURCE = join(ROOT, "plugin", "tui.tsx")
30
+
31
+ /** Generated JavaScript entry shipped as `./tui`. */
32
+ const TARGET = join(ROOT, "plugin", "tui.js")
33
+
34
+ /** Header marking the artifact as generated. */
35
+ const HEADER =
36
+ "// Generated from plugin/tui.tsx — do not edit; run `npm run build:tui` to regenerate.\n"
37
+
38
+ /**
39
+ * Load `@opentui/solid`'s Solid JSX transform.
40
+ *
41
+ * The transform module is not part of the package's public exports map, so it
42
+ * is resolved through the package root: resolve the package entry point, then
43
+ * import the transform beside it.
44
+ *
45
+ * @returns The `transformSolidSource(code, options)` function.
46
+ */
47
+ async function loadTransform() {
48
+ const require = createRequire(join(ROOT, "scripts", "build-tui.mjs"))
49
+ const packageEntry = require.resolve("@opentui/solid")
50
+ const modulePath = join(dirname(packageEntry), "scripts", "solid-transform.js")
51
+ const module = await import(pathToFileURL(modulePath).href)
52
+ return module.transformSolidSource
53
+ }
54
+
55
+ /**
56
+ * Compile the current `plugin/tui.tsx` into the shipped entry source.
57
+ *
58
+ * @returns The complete `plugin/tui.js` content, header included.
59
+ */
60
+ export async function buildTuiSource() {
61
+ const transformSolidSource = await loadTransform()
62
+ const source = readFileSync(SOURCE, "utf8")
63
+ const transformed = await transformSolidSource(source, { filename: SOURCE })
64
+ const code = typeof transformed === "string" ? transformed : transformed.code
65
+ return HEADER + code
66
+ }
67
+
68
+ // When run directly (`npm run build:tui`), regenerate the committed artifact.
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
+ const code = await buildTuiSource()
71
+ writeFileSync(TARGET, code)
72
+ console.log(`built plugin/tui.js (${code.length} chars)`)
73
+ }