opencode-goal-mode 0.3.4 → 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 +36 -0
- package/README.md +35 -15
- package/docs/benchmarks/latency.svg +3 -3
- package/docs/benchmarks/results.json +4 -4
- package/package.json +13 -2
- 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,41 @@
|
|
|
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
|
+
|
|
22
|
+
## v0.3.5
|
|
23
|
+
|
|
24
|
+
- Verified the package against the **current** OpenCode plugin API
|
|
25
|
+
(`@opencode-ai/plugin@1.17.6`, matching OpenCode 1.17.6, now the dev pin): all
|
|
26
|
+
guard hooks (`chat.message`/`params`, `tool.execute.before`/`after`,
|
|
27
|
+
`experimental.chat.system.transform`/`text.complete`/`session.compacting`)
|
|
28
|
+
exist; the guard plugin loads with zero errors; and in a real OpenCode the
|
|
29
|
+
agent list shows `goal` as the only user-selectable agent with 26 subagents and
|
|
30
|
+
reviewer `edit`/`task: deny` applied. The enforcement core is unchanged and
|
|
31
|
+
fully intact.
|
|
32
|
+
- Declared `@opentui/solid`, `solid-js`, `@opencode-ai/plugin` as **optional**
|
|
33
|
+
peer dependencies (the TUI runtime the sidebar uses).
|
|
34
|
+
- Docs: stated the sidebar's verification status honestly — the experimental TUI
|
|
35
|
+
banner is verified to load and to render in a real headless OpenTUI test, but
|
|
36
|
+
its live in-session render depends on your OpenCode build's file-based
|
|
37
|
+
TUI-plugin support; it never errors and never affects the enforcement core.
|
|
38
|
+
|
|
3
39
|
## v0.3.4
|
|
4
40
|
|
|
5
41
|
Critical fixes found by testing against a real OpenCode (1.17.6) install — the
|
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
|
|
@@ -41,8 +45,9 @@ opencode agent list | grep goal
|
|
|
41
45
|
The `goal` agent writes a contract, delegates research/review to subagents, and
|
|
42
46
|
**cannot** answer `Goal Completed` until every required review gate passes — the
|
|
43
47
|
guard rewrites a premature claim to `Goal Not Completed`. Try a destructive
|
|
44
|
-
command mid-session (e.g. `rm -rf build`) and watch it get blocked.
|
|
45
|
-
goal
|
|
48
|
+
command mid-session (e.g. `rm -rf build`) and watch it get blocked. If your
|
|
49
|
+
OpenCode build supports TUI plugins, the active goal also appears in the sidebar
|
|
50
|
+
in yellow (experimental — see [TUI integration](#tui-integration)).
|
|
46
51
|
|
|
47
52
|
That's it. Everything below is detail.
|
|
48
53
|
|
|
@@ -171,17 +176,31 @@ Goal Mode is a **plugin pair**: the server-side `goal-guard` plugin owns
|
|
|
171
176
|
enforcement and writes its state to disk, and an experimental TUI plugin
|
|
172
177
|
(`plugins/goal-sidebar.js`) reads that same state to render a live banner.
|
|
173
178
|
|
|
174
|
-
- **Sidebar goal banner
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
185
204
|
- **Toasts.** Review verdicts and completion-unlock events surface as toasts
|
|
186
205
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
187
206
|
toast as before (`toastOnBlock`).
|
|
@@ -262,8 +281,9 @@ Or via environment variables (`GOAL_GUARD_*`):
|
|
|
262
281
|
| `toastOnBlock` / `GOAL_GUARD_TOAST_ON_BLOCK` | `true` | Toast when something is blocked. |
|
|
263
282
|
| `toastOnReview` / `GOAL_GUARD_TOAST_ON_REVIEW` | `true` | Toast on each review verdict and when completion unlocks. |
|
|
264
283
|
| `sidebarBanner` / `GOAL_GUARD_SIDEBAR_BANNER` | `true` | Show the experimental yellow goal banner in the TUI sidebar. |
|
|
265
|
-
| `sidebarColor` / `GOAL_GUARD_SIDEBAR_COLOR` | `#FFD700` |
|
|
266
|
-
| `
|
|
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). |
|
|
267
287
|
|
|
268
288
|
## Custom tools
|
|
269
289
|
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<text x="20" y="47" font-size="12" fill="#656d76">Microseconds to classify one command. Both are negligible for a tool-call guard.</text>
|
|
5
5
|
<text x="218" y="87" font-size="12" text-anchor="end" fill="#1f2328">Legacy regex guard</text>
|
|
6
6
|
<rect x="230" y="70" width="420" height="22" rx="3" fill="#eaeef2"/>
|
|
7
|
-
<rect x="230" y="70" width="
|
|
8
|
-
<text x="
|
|
7
|
+
<rect x="230" y="70" width="217.1" height="22" rx="3" fill="#9aa0a6"/>
|
|
8
|
+
<text x="455.1" y="87" font-size="12" font-weight="600" fill="#1f2328">0.75 µs</text>
|
|
9
9
|
<text x="218" y="125" font-size="12" text-anchor="end" fill="#1f2328">Goal Mode analyzer</text>
|
|
10
10
|
<rect x="230" y="108" width="420" height="22" rx="3" fill="#eaeef2"/>
|
|
11
11
|
<rect x="230" y="108" width="300.0" height="22" rx="3" fill="#2da44e"/>
|
|
12
|
-
<text x="538.0" y="125" font-size="12" font-weight="600" fill="#1f2328">1.
|
|
12
|
+
<text x="538.0" y="125" font-size="12" font-weight="600" fill="#1f2328">1.03 µs</text>
|
|
13
13
|
</svg>
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"safeFalsePos": 5
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
|
-
"opsPerSec":
|
|
79
|
-
"usPerCommand": 0.
|
|
78
|
+
"opsPerSec": 1341168,
|
|
79
|
+
"usPerCommand": 0.75
|
|
80
80
|
},
|
|
81
81
|
"current": {
|
|
82
82
|
"detectionRate": 100,
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
"safeFalsePos": 0
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
|
-
"opsPerSec":
|
|
115
|
-
"usPerCommand": 1.
|
|
114
|
+
"opsPerSec": 970526,
|
|
115
|
+
"usPerCommand": 1.03
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"completionFixtures": {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-mode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "plugins/goal-sidebar.js",
|
|
6
7
|
"engines": {
|
|
7
8
|
"node": ">=20.11"
|
|
8
9
|
},
|
|
@@ -82,8 +83,18 @@
|
|
|
82
83
|
"registry": "https://registry.npmjs.org/"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
|
-
"@opencode-ai/plugin": "1.
|
|
86
|
+
"@opencode-ai/plugin": "1.17.6",
|
|
86
87
|
"fast-check": "^4.8.0",
|
|
87
88
|
"yaml": "^2.6.1"
|
|
89
|
+
},
|
|
90
|
+
"peerDependencies": {
|
|
91
|
+
"@opencode-ai/plugin": ">=1.15.0",
|
|
92
|
+
"@opentui/solid": "*",
|
|
93
|
+
"solid-js": "*"
|
|
94
|
+
},
|
|
95
|
+
"peerDependenciesMeta": {
|
|
96
|
+
"@opencode-ai/plugin": { "optional": true },
|
|
97
|
+
"@opentui/solid": { "optional": true },
|
|
98
|
+
"solid-js": { "optional": true }
|
|
88
99
|
}
|
|
89
100
|
}
|
|
@@ -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.");
|