opencode-goal-mode 0.3.5 → 0.3.6
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 +19 -0
- package/README.md +32 -20
- package/package.json +2 -1
- package/plugins/goal-guard/config.js +4 -1
- package/plugins/goal-guard/summary.js +34 -9
- package/plugins/goal-sidebar.js +42 -39
- package/scripts/install.mjs +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.6
|
|
4
|
+
|
|
5
|
+
- **FIX: the sidebar never loaded.** TUI plugins are loaded from
|
|
6
|
+
`~/.config/opencode/tui.json` — NOT the `plugins/` dir (that is server plugins
|
|
7
|
+
only). The installer never created tui.json, so OpenCode never loaded the
|
|
8
|
+
sidebar (it only showed the session title + context). The installer now
|
|
9
|
+
registers the sidebar in `tui.json` (`plugin: ["opencode-goal-mode"]`,
|
|
10
|
+
merge-safe), and the package `main` points at the TUI plugin so OpenCode loads
|
|
11
|
+
it with its `@opentui/solid` runtime. Confirmed OpenCode reads tui.json.
|
|
12
|
+
- **Sidebar behaviour, as requested:** under the sidebar's content/"context"
|
|
13
|
+
area it now shows the goal with generated status text, colour-coded by
|
|
14
|
+
lifecycle: **yellow** while the goal is running, **red** when it is done (all
|
|
15
|
+
required gates pass, tree clean), and **grey "No goal available"** when a task
|
|
16
|
+
is running with no goal set. New `sidebarDoneColor` option
|
|
17
|
+
(`GOAL_GUARD_SIDEBAR_DONE_COLOR`, default `#FF5555`).
|
|
18
|
+
- `summary.sidebarView` now returns `{ state: "none"|"running"|"done", goal,
|
|
19
|
+
detail }`; the headless visual test renders and asserts all three states (text
|
|
20
|
+
+ exact colours), 18/18.
|
|
21
|
+
|
|
3
22
|
## v0.3.5
|
|
4
23
|
|
|
5
24
|
- Verified the package against the **current** OpenCode plugin API
|
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ npm install -g opencode-goal-mode && opencode-goal-mode-install --global
|
|
|
18
18
|
|
|
19
19
|

|
|
20
20
|
|
|
21
|
+
<sub>↑ Illustrative mockup of the **experimental** sidebar banner. The enforcement
|
|
22
|
+
core (guard + agents) is the verified product; the TUI sidebar is opt-in and its
|
|
23
|
+
live render depends on your OpenCode build — see [TUI integration](#tui-integration).</sub>
|
|
24
|
+
|
|
21
25
|
**[Quick start](#quick-start) · [Install](#install) · [Why it's different](#why-its-different) · [Benchmarks](#benchmarks-honest-edition) · [TUI integration](#tui-integration) · [Configuration](#configuration) · [Releasing](#releasing) · [Architecture](ARCHITECTURE.md)**
|
|
22
26
|
|
|
23
27
|
## Quick start
|
|
@@ -172,24 +176,31 @@ Goal Mode is a **plugin pair**: the server-side `goal-guard` plugin owns
|
|
|
172
176
|
enforcement and writes its state to disk, and an experimental TUI plugin
|
|
173
177
|
(`plugins/goal-sidebar.js`) reads that same state to render a live banner.
|
|
174
178
|
|
|
175
|
-
- **Sidebar goal banner
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
OpenCode
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
179
|
+
- **Sidebar goal banner.** In the sidebar's content area, under the session
|
|
180
|
+
title/context, it shows the current goal with generated status text, colour-coded
|
|
181
|
+
by lifecycle:
|
|
182
|
+
- **yellow** — a goal is set and running (`◆ GOAL …` + `in progress · N/M gates`);
|
|
183
|
+
- **red** — the goal is done (all required gates pass, tree clean: `✓ GOAL …` +
|
|
184
|
+
`completed · N/M gates passed · K review cycles`);
|
|
185
|
+
- **grey** — a task is running with no goal set (`No goal available`).
|
|
186
|
+
|
|
187
|
+
Toggle/recolour with `sidebarBanner`, `sidebarColor` (running), `sidebarDoneColor`
|
|
188
|
+
(done), `sidebarMutedColor` (no goal), or the `GOAL_GUARD_SIDEBAR_*` env vars.
|
|
189
|
+
|
|
190
|
+
**How it loads — important.** TUI plugins are **not** loaded from the `plugins/`
|
|
191
|
+
dir; OpenCode loads them from `~/.config/opencode/tui.json`. The installer writes
|
|
192
|
+
that for you (merge-safe):
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{ "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Restart OpenCode after install so it picks up the TUI plugin (it resolves the
|
|
199
|
+
package and provides the `@opentui/solid` runtime). The banner appears in a
|
|
200
|
+
**session** view (not the home screen). The three states are rendered and
|
|
201
|
+
asserted — text + exact colours — by a real headless OpenTUI renderer in the
|
|
202
|
+
[visual test](tools/visual-test/README.md) (`npm run test:visual`, 18/18). The
|
|
203
|
+
enforcement core is a separate server plugin and works regardless of the sidebar.
|
|
193
204
|
- **Toasts.** Review verdicts and completion-unlock events surface as toasts
|
|
194
205
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
195
206
|
toast as before (`toastOnBlock`).
|
|
@@ -270,8 +281,9 @@ Or via environment variables (`GOAL_GUARD_*`):
|
|
|
270
281
|
| `toastOnBlock` / `GOAL_GUARD_TOAST_ON_BLOCK` | `true` | Toast when something is blocked. |
|
|
271
282
|
| `toastOnReview` / `GOAL_GUARD_TOAST_ON_REVIEW` | `true` | Toast on each review verdict and when completion unlocks. |
|
|
272
283
|
| `sidebarBanner` / `GOAL_GUARD_SIDEBAR_BANNER` | `true` | Show the experimental yellow goal banner in the TUI sidebar. |
|
|
273
|
-
| `sidebarColor` / `GOAL_GUARD_SIDEBAR_COLOR` | `#FFD700` |
|
|
274
|
-
| `
|
|
284
|
+
| `sidebarColor` / `GOAL_GUARD_SIDEBAR_COLOR` | `#FFD700` | Colour of a **running** goal in the sidebar (yellow). |
|
|
285
|
+
| `sidebarDoneColor` / `GOAL_GUARD_SIDEBAR_DONE_COLOR` | `#FF5555` | Colour of a **done** goal in the sidebar (red). |
|
|
286
|
+
| `sidebarMutedColor` / `GOAL_GUARD_SIDEBAR_MUTED_COLOR` | `#808080` | Colour of the "No goal available" line (grey). |
|
|
275
287
|
|
|
276
288
|
## Custom tools
|
|
277
289
|
|
package/package.json
CHANGED
|
@@ -32,7 +32,9 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
32
32
|
sidebarBanner: true,
|
|
33
33
|
/** Foreground colour (hex) for the sidebar goal banner. */
|
|
34
34
|
sidebarColor: "#FFD700",
|
|
35
|
-
/** Foreground colour (hex) for
|
|
35
|
+
/** Foreground colour (hex) for a completed goal in the sidebar (running → done turns yellow → red). */
|
|
36
|
+
sidebarDoneColor: "#FF5555",
|
|
37
|
+
/** Foreground colour (hex) for the muted "No goal available" sidebar line. */
|
|
36
38
|
sidebarMutedColor: "#808080",
|
|
37
39
|
/** Phrase that, at the start of an assistant message, claims completion. */
|
|
38
40
|
completionMarker: "Goal Completed",
|
|
@@ -70,6 +72,7 @@ function fromEnv(env) {
|
|
|
70
72
|
GOAL_GUARD_TOAST_ON_REVIEW: ["toastOnReview", coerceBool],
|
|
71
73
|
GOAL_GUARD_SIDEBAR_BANNER: ["sidebarBanner", coerceBool],
|
|
72
74
|
GOAL_GUARD_SIDEBAR_COLOR: ["sidebarColor", (v) => (v == null ? undefined : String(v))],
|
|
75
|
+
GOAL_GUARD_SIDEBAR_DONE_COLOR: ["sidebarDoneColor", (v) => (v == null ? undefined : String(v))],
|
|
73
76
|
GOAL_GUARD_SIDEBAR_MUTED_COLOR: ["sidebarMutedColor", (v) => (v == null ? undefined : String(v))],
|
|
74
77
|
};
|
|
75
78
|
for (const [key, [field, coerce]] of Object.entries(map)) {
|
|
@@ -21,14 +21,18 @@ export function shortGoalLabel(state, max = 80) {
|
|
|
21
21
|
return `${base.slice(0, max - 1).trimEnd()}…`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Sentinel for "a task is running but no goal is set" — the sidebar shows a muted "No goal". */
|
|
25
|
-
export const NO_GOAL = Object.freeze({
|
|
24
|
+
/** Sentinel for "a task is running but no goal is set" — the sidebar shows a muted "No goal available". */
|
|
25
|
+
export const NO_GOAL = Object.freeze({ state: "none", goal: "", detail: "" });
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Compact projection for the TUI sidebar banner. ALWAYS returns an object
|
|
29
|
-
* sidebar
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
28
|
+
* Compact projection for the TUI sidebar banner. ALWAYS returns an object with a
|
|
29
|
+
* three-way `state` so the sidebar renders unconditionally:
|
|
30
|
+
* - `state: "none"` → no active goal: grey "No goal available".
|
|
31
|
+
* - `state: "running"` → goal in progress: yellow, with a generated status line.
|
|
32
|
+
* - `state: "done"` → goal complete (all required gates pass, tree clean):
|
|
33
|
+
* red, with a generated completion line.
|
|
34
|
+
* `goal` is the short goal label; `detail` is generated descriptive text derived
|
|
35
|
+
* from the current goal's gate/cycle/dirty state.
|
|
32
36
|
*/
|
|
33
37
|
export function sidebarView(state, config) {
|
|
34
38
|
if (!state || !state.active) return NO_GOAL;
|
|
@@ -37,9 +41,30 @@ export function sidebarView(state, config) {
|
|
|
37
41
|
const required = requiredGates(state, config);
|
|
38
42
|
const missing = missingGates(state, config);
|
|
39
43
|
const passing = required.length - missing.length;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
44
|
+
const cycles = Number(state.reviewCycles) || 0;
|
|
45
|
+
const done = required.length > 0 && missing.length === 0 && !state.dirty;
|
|
46
|
+
if (done) {
|
|
47
|
+
return {
|
|
48
|
+
state: "done",
|
|
49
|
+
goal,
|
|
50
|
+
detail: `completed · ${passing}/${required.length} gates passed · ${cycles} review cycle${cycles === 1 ? "" : "s"}`,
|
|
51
|
+
passing,
|
|
52
|
+
required: required.length,
|
|
53
|
+
reviewCycles: cycles,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const bits = [`${passing}/${required.length} gates`];
|
|
57
|
+
if (state.dirty) bits.push("changes pending");
|
|
58
|
+
if (cycles) bits.push(`cycle ${cycles}`);
|
|
59
|
+
return {
|
|
60
|
+
state: "running",
|
|
61
|
+
goal,
|
|
62
|
+
detail: `in progress · ${bits.join(" · ")}`,
|
|
63
|
+
passing,
|
|
64
|
+
required: required.length,
|
|
65
|
+
reviewCycles: cycles,
|
|
66
|
+
dirty: Boolean(state.dirty),
|
|
67
|
+
};
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
export function summarizeState(state, config) {
|
package/plugins/goal-sidebar.js
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
/**
|
|
3
|
-
* Goal Mode —
|
|
3
|
+
* Goal Mode — TUI sidebar goal banner.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* This is a TUI plugin module (companion to the server-side goal-guard plugin).
|
|
6
|
+
* It renders, in the sidebar's content area, the current goal with generated
|
|
7
|
+
* status text and a colour that tracks the goal's lifecycle:
|
|
8
|
+
* - RUNNING (goal set, in progress) → yellow
|
|
9
|
+
* - DONE (all required gates pass, clean) → red
|
|
10
|
+
* - NONE (a task is running, no goal set) → grey "No goal available"
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
12
|
+
* IMPORTANT — how OpenCode loads this: TUI plugins are NOT loaded from the
|
|
13
|
+
* regular `plugin` array / plugins dir (that is server plugins). They are listed
|
|
14
|
+
* in `~/.config/opencode/tui.json`:
|
|
15
|
+
* { "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }
|
|
16
|
+
* The installer writes that automatically. OpenCode then loads this module
|
|
17
|
+
* (the package `main`) and provides the `@opentui/solid` + `solid-js` runtime
|
|
18
|
+
* (declared here as peer deps), so its slot shares OpenCode's renderer.
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Runtime constraints (mirrored from working OpenCode TUI plugins):
|
|
21
|
-
* - TUI plugin modules export `export default { id, tui }`.
|
|
22
|
-
* - The Bun TUI plugin runtime does NOT support top-level ESM imports of Node
|
|
23
|
-
* built-ins, so `node:fs`/`node:path`/`node:os`/`node:crypto` are `require()`d
|
|
24
|
-
* lazily inside functions. Top-level imports of regular packages (solid-js)
|
|
25
|
-
* and of our Node-built-in-free local modules are fine.
|
|
26
|
-
* - This file uses Solid/opentui JSX and is loaded only by OpenCode's (Bun) TUI
|
|
27
|
-
* runtime, which transpiles it; it is never imported by the Node test suite.
|
|
20
|
+
* Runtime constraints (from working OpenCode TUI plugins):
|
|
21
|
+
* - the module exports a single `export default { id, tui }`;
|
|
22
|
+
* - the Bun TUI runtime does not support top-level ESM imports of Node built-ins,
|
|
23
|
+
* so node:fs/path/os/crypto are require()d lazily inside functions;
|
|
24
|
+
* - it is never imported by the Node test suite (the pure projection it uses,
|
|
25
|
+
* summary.sidebarView, is tested via goal-guard/sidebar-data.js).
|
|
28
26
|
*/
|
|
29
27
|
|
|
30
28
|
import { createSignal, onCleanup, Show } from "solid-js";
|
|
31
29
|
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
32
30
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
33
31
|
|
|
34
|
-
const DEFAULT_COLOR = "#FFD700"; // shining yellow
|
|
35
|
-
const
|
|
32
|
+
const DEFAULT_COLOR = "#FFD700"; // shining yellow — running
|
|
33
|
+
const DEFAULT_DONE = "#FF5555"; // red — completed
|
|
34
|
+
const DEFAULT_MUTED = "#808080"; // grey — no goal
|
|
36
35
|
const POLL_MS = 1500;
|
|
37
36
|
|
|
38
37
|
function resolveOptions(options, env) {
|
|
@@ -41,16 +40,15 @@ function resolveOptions(options, env) {
|
|
|
41
40
|
const enabledEnv = e.GOAL_GUARD_SIDEBAR_BANNER;
|
|
42
41
|
const disabled =
|
|
43
42
|
enabledOpt === false || enabledEnv === "0" || enabledEnv === "false" || enabledEnv === "off";
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
return {
|
|
44
|
+
enabled: !disabled,
|
|
45
|
+
color: options?.sidebarColor || e.GOAL_GUARD_SIDEBAR_COLOR || DEFAULT_COLOR,
|
|
46
|
+
doneColor: options?.sidebarDoneColor || e.GOAL_GUARD_SIDEBAR_DONE_COLOR || DEFAULT_DONE,
|
|
47
|
+
muted: options?.sidebarMutedColor || e.GOAL_GUARD_SIDEBAR_MUTED_COLOR || DEFAULT_MUTED,
|
|
48
|
+
};
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/**
|
|
50
|
-
* Read the guard's persisted snapshot for a worktree. The state-path logic is
|
|
51
|
-
* kept identical to goal-guard/persistence.js (stateBaseDir + projectKey); node
|
|
52
|
-
* built-ins are required lazily to satisfy the TUI runtime.
|
|
53
|
-
*/
|
|
51
|
+
/** Read the guard's persisted snapshot for a worktree (path logic mirrors persistence.js). */
|
|
54
52
|
function readSnapshot(worktree) {
|
|
55
53
|
try {
|
|
56
54
|
const fs = require("node:fs");
|
|
@@ -100,7 +98,7 @@ const id = "goal-mode-sidebar";
|
|
|
100
98
|
/** @type {import("@opencode-ai/plugin/tui").TuiPlugin} */
|
|
101
99
|
const tui = async (api, options) => {
|
|
102
100
|
try {
|
|
103
|
-
const { enabled, color, muted } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
101
|
+
const { enabled, color, doneColor, muted } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
104
102
|
if (!enabled) return;
|
|
105
103
|
if (!api?.slots?.register) return; // runtime without the slot API → no-op.
|
|
106
104
|
|
|
@@ -120,16 +118,21 @@ const tui = async (api, options) => {
|
|
|
120
118
|
const [model, setModel] = createSignal(read());
|
|
121
119
|
const timer = setInterval(() => setModel(read()), POLL_MS);
|
|
122
120
|
onCleanup(() => clearInterval(timer));
|
|
123
|
-
|
|
121
|
+
const fg = () => (model().state === "done" ? doneColor : color);
|
|
122
|
+
// Always render: grey "No goal available" when none, else the goal in
|
|
123
|
+
// yellow (running) / red (done) with generated status text.
|
|
124
124
|
return (
|
|
125
|
-
<box flexDirection="column">
|
|
126
|
-
<Show
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
<box flexDirection="column" paddingTop={1}>
|
|
126
|
+
<Show
|
|
127
|
+
when={model().state !== "none"}
|
|
128
|
+
fallback={<text fg={muted}>No goal available</text>}
|
|
129
|
+
>
|
|
130
|
+
<text fg={fg()}>
|
|
131
|
+
{model().state === "done" ? "✓ " : "◆ "}
|
|
129
132
|
<b>GOAL</b>
|
|
130
133
|
{` ${model().goal}`}
|
|
131
134
|
</text>
|
|
132
|
-
<text fg={
|
|
135
|
+
<text fg={fg()}>{model().detail}</text>
|
|
133
136
|
</Show>
|
|
134
137
|
</box>
|
|
135
138
|
);
|
package/scripts/install.mjs
CHANGED
|
@@ -125,6 +125,36 @@ function loadManifest() {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Register (or remove) the TUI sidebar plugin in `<target>/tui.json`.
|
|
130
|
+
*
|
|
131
|
+
* TUI plugins are NOT loaded from the `plugins/` dir (that path is for server
|
|
132
|
+
* plugins); OpenCode loads them only from tui.json. We reference the published
|
|
133
|
+
* npm package by name so OpenCode resolves it with its `@opentui/solid` runtime.
|
|
134
|
+
* Merge-safe: preserves any existing entries and only touches our own.
|
|
135
|
+
*/
|
|
136
|
+
const TUI_PLUGIN_SPEC = pkg.name;
|
|
137
|
+
function ensureTuiPlugin(remove = false) {
|
|
138
|
+
const tuiPath = join(target, "tui.json");
|
|
139
|
+
let data = { $schema: "https://opencode.ai/tui.json", plugin: [] };
|
|
140
|
+
try {
|
|
141
|
+
const existing = JSON.parse(readFileSync(tuiPath, "utf8"));
|
|
142
|
+
if (existing && typeof existing === "object") data = existing;
|
|
143
|
+
} catch {
|
|
144
|
+
/* missing or invalid → start fresh */
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(data.plugin)) data.plugin = [];
|
|
147
|
+
const has = data.plugin.includes(TUI_PLUGIN_SPEC);
|
|
148
|
+
if (remove ? !has : has) return false;
|
|
149
|
+
data.plugin = remove ? data.plugin.filter((p) => p !== TUI_PLUGIN_SPEC) : [...data.plugin, TUI_PLUGIN_SPEC];
|
|
150
|
+
if (!data.$schema) data.$schema = "https://opencode.ai/tui.json";
|
|
151
|
+
if (!values["dry-run"]) {
|
|
152
|
+
mkdirSync(target, { recursive: true });
|
|
153
|
+
writeFileSync(tuiPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
// ---------------------------------------------------------------------------
|
|
129
159
|
// Uninstall
|
|
130
160
|
// ---------------------------------------------------------------------------
|
|
@@ -144,7 +174,9 @@ if (values.uninstall) {
|
|
|
144
174
|
}
|
|
145
175
|
}
|
|
146
176
|
if (!values["dry-run"] && existsSync(manifestPath)) rmSync(manifestPath, { force: true });
|
|
177
|
+
const tuiRemoved = ensureTuiPlugin(true);
|
|
147
178
|
if (!values["dry-run"]) pruneEmptyDirs(target, Object.keys(manifest.files));
|
|
179
|
+
if (tuiRemoved) console.log(`${values["dry-run"] ? "Would remove" : "Removed"} the sidebar entry from ${join(target, "tui.json")}`);
|
|
148
180
|
const verb = values["dry-run"] ? "Would remove" : "Removed";
|
|
149
181
|
console.log(`${verb} ${removed.length} Goal Mode files from ${target}.`);
|
|
150
182
|
if (kept.length) {
|
|
@@ -234,9 +266,12 @@ if (!values["dry-run"]) {
|
|
|
234
266
|
writeFileSync(manifestPath, JSON.stringify({ version: pkg.version, files: newManifestFiles }, null, 2), "utf8");
|
|
235
267
|
}
|
|
236
268
|
|
|
269
|
+
const tuiAdded = ensureTuiPlugin(false);
|
|
270
|
+
|
|
237
271
|
const verb = values["dry-run"] ? "Would install" : "Installed";
|
|
238
272
|
console.log(`${verb} OpenCode Goal Mode ${pkg.version} into ${target}`);
|
|
239
273
|
console.log(
|
|
240
274
|
`Files copied: ${summary.copied.length}; unchanged: ${summary.unchanged.length}; pruned: ${summary.pruned.length}`,
|
|
241
275
|
);
|
|
276
|
+
if (tuiAdded) console.log(`Registered the experimental sidebar in ${join(target, "tui.json")}`);
|
|
242
277
|
console.log("Restart OpenCode for agents, commands, and plugins to load.");
|