opencode-goal-mode 0.4.0 → 0.4.2
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 +69 -0
- package/README.md +67 -41
- package/agents/goal.md +2 -2
- package/docs/sidebar-demo.svg +46 -37
- package/package.json +1 -1
- package/plugins/goal-guard/guard.js +7 -0
- package/plugins/goal-guard/summary.js +44 -19
- package/plugins/goal-sidebar.tsx +91 -34
- package/scripts/install.mjs +48 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.2
|
|
4
|
+
|
|
5
|
+
### The sidebar Goal section now actually renders in the live TUI
|
|
6
|
+
|
|
7
|
+
- **Root-cause fix.** The `sidebar_content` slot bailed at mount with
|
|
8
|
+
`return undefined` whenever no goal existed *yet* — but the goal is normally set
|
|
9
|
+
*after* the sidebar mounts, so the polling component never started and the Goal
|
|
10
|
+
section never appeared. The slot now **always mounts a reactive, polling
|
|
11
|
+
component** and reveals the section (via `<Show>`) the moment the goal is
|
|
12
|
+
recorded. Verified by driving the real OpenCode TUI in a PTY.
|
|
13
|
+
- **Stale plugin cache.** OpenCode caches TUI plugins under
|
|
14
|
+
`~/.cache/opencode/packages/<name>@<spec>/` and never re-checks npm, so upgrades
|
|
15
|
+
kept loading the *old* sidebar build. The installer now clears that cache on
|
|
16
|
+
install and uninstall, so a restart picks up the installed version.
|
|
17
|
+
- The first-display rainbow now triggers when the goal first appears (not at
|
|
18
|
+
mount, when there may be no goal yet).
|
|
19
|
+
- The sidebar resolves state under both the worktree and directory path keys, so a
|
|
20
|
+
path-key mismatch can't hide an active goal.
|
|
21
|
+
|
|
22
|
+
### Sidebar layout
|
|
23
|
+
|
|
24
|
+
- The gate count and the lifecycle status are now on **separate lines**, each in
|
|
25
|
+
its own colour (GOAL = yellow, title = white, gates = cyan, status = orange).
|
|
26
|
+
|
|
27
|
+
### Native todos are replaced in goal mode
|
|
28
|
+
|
|
29
|
+
- The `goal` agent no longer uses the native `todowrite` tool (it is disabled in
|
|
30
|
+
Goal Mode). Because OpenCode renders native todos as their own sidebar slot, the
|
|
31
|
+
only way to replace them is to stop producing them — so in a goal session the
|
|
32
|
+
native todo list stays empty and the structured Goal-owned section is what shows.
|
|
33
|
+
Build and every other mode keep their native todos.
|
|
34
|
+
|
|
35
|
+
## v0.4.1
|
|
36
|
+
|
|
37
|
+
### Restructured Goal sidebar todo section
|
|
38
|
+
|
|
39
|
+
- The sidebar Goal section is now a proper stacked, multi-colour layout instead of
|
|
40
|
+
one run of text: a bold **`GOAL`** label on its own line (yellow while running,
|
|
41
|
+
red when done), then the goal title, then a `passing/total gates · status` line —
|
|
42
|
+
each line in its own highlight colour (GOAL yellow, title white, status cyan) so
|
|
43
|
+
they never blend together. It opens with a first-display per-line rainbow, then
|
|
44
|
+
settles.
|
|
45
|
+
- Removed the noisy `· changes pending` suffix from the status line; pending work
|
|
46
|
+
now surfaces as a structured todo row instead.
|
|
47
|
+
- Better, more structured todos: one row per acceptance criterion (✓ when fresh
|
|
48
|
+
evidence covers it), a re-verify row when the tree changed, and one row per
|
|
49
|
+
still-missing review gate by friendly name (e.g. "Pass Security Reviewer").
|
|
50
|
+
- In a goal session the section takes over the sidebar `sidebar_content` slot;
|
|
51
|
+
because OpenCode renders the native todo list as that slot's fallback, it
|
|
52
|
+
replaces the native list on builds that use replace/single-winner slot mode and
|
|
53
|
+
sits alongside it otherwise. Non-goal and no-goal sessions render nothing, so
|
|
54
|
+
the native todo list stays in place.
|
|
55
|
+
|
|
56
|
+
### Build mode is never treated as a goal
|
|
57
|
+
|
|
58
|
+
- Hardened the guard so a Build/Plan/custom session never accumulates goal state.
|
|
59
|
+
`tool.execute.after` bookkeeping (dirty flag, edits, verification, verdicts) now
|
|
60
|
+
runs only for active Goal sessions (and goal-namespace subagent sessions for
|
|
61
|
+
verdict capture). Destructive-command blocking still applies in every mode.
|
|
62
|
+
|
|
63
|
+
### Installer
|
|
64
|
+
|
|
65
|
+
- `--global` now resolves the home directory via `$HOME` and falls back to the OS
|
|
66
|
+
home dir, so it works in shells/containers where `$HOME` is unset.
|
|
67
|
+
- Verified the full install → idempotent re-run → conflict-protection → uninstall
|
|
68
|
+
lifecycle end-to-end on macOS (Node 24) and in clean Linux containers (Apple
|
|
69
|
+
`container`, Node 20 and Node 24) using the real `npm install -g <tarball>` path.
|
|
70
|
+
- Moved **Install** to the top of the README and documented dry-run/uninstall.
|
|
71
|
+
|
|
3
72
|
## v0.4.0
|
|
4
73
|
|
|
5
74
|
### Goal-only subagents
|
package/README.md
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
# OpenCode Goal Mode
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
4
|
-
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
5
|
-
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/ci.yml)
|
|
6
|
-
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/publish.yml)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
[](package.json)
|
|
9
|
-
|
|
10
3
|
Strict Goal Mode for OpenCode: a primary `goal` agent, specialized review
|
|
11
4
|
subagents, slash commands, a `goal-guard` plugin that enforces review discipline
|
|
12
|
-
and blocks destructive shell commands, and a
|
|
13
|
-
TUI sidebar.
|
|
5
|
+
and blocks destructive shell commands, and a structured Goal-owned todo section
|
|
6
|
+
in the TUI sidebar.
|
|
14
7
|
|
|
15
8
|
## Install
|
|
16
9
|
|
|
17
|
-
**One command
|
|
10
|
+
**One command.** Needs [Node](https://nodejs.org) 20.11+ and a working
|
|
11
|
+
[OpenCode](https://opencode.ai). Works on macOS and Linux:
|
|
18
12
|
|
|
19
13
|
```bash
|
|
20
14
|
npm install -g opencode-goal-mode && opencode-goal-mode --global
|
|
21
15
|
```
|
|
22
16
|
|
|
23
|
-
Then **restart OpenCode**. That's the whole install
|
|
17
|
+
Then **restart OpenCode**. That's the whole install — it copies the Goal agent,
|
|
24
18
|
review subagents, slash commands, and guard plugin into `~/.config/opencode`, and
|
|
25
19
|
merge-safely registers the Goal todo sidebar in `~/.config/opencode/tui.json`.
|
|
26
20
|
In the agent picker you'll see only the **`goal`** agent; reviewers are subagents
|
|
27
|
-
it drives automatically. The
|
|
28
|
-
|
|
21
|
+
it drives automatically. The install is **idempotent** (re-run it to upgrade in
|
|
22
|
+
place), never touches files you've edited, and `--uninstall` removes exactly what
|
|
23
|
+
it added. Goal Mode inherits your existing OpenCode model/provider.
|
|
29
24
|
|
|
30
25
|
<details>
|
|
31
26
|
<summary>Other ways to install</summary>
|
|
32
27
|
|
|
33
28
|
```bash
|
|
34
|
-
# Global npm install, then run the installer
|
|
29
|
+
# Global npm install, then run the installer separately
|
|
35
30
|
npm install -g opencode-goal-mode
|
|
36
31
|
opencode-goal-mode --global # alias of opencode-goal-mode-install
|
|
37
32
|
|
|
33
|
+
# Preview first, then install (no writes on --dry-run)
|
|
34
|
+
opencode-goal-mode --global --dry-run
|
|
35
|
+
|
|
38
36
|
# Temporary npx install (server-side components work; for the TUI sidebar,
|
|
39
37
|
# prefer the global install above so OpenCode can resolve the package later)
|
|
40
38
|
npx opencode-goal-mode --global
|
|
@@ -42,6 +40,9 @@ npx opencode-goal-mode --global
|
|
|
42
40
|
# Into a single project (writes ./.opencode, including ./.opencode/tui.json)
|
|
43
41
|
npx opencode-goal-mode
|
|
44
42
|
|
|
43
|
+
# Clean removal of everything it installed (incl. its tui.json entry)
|
|
44
|
+
opencode-goal-mode --global --uninstall
|
|
45
|
+
|
|
45
46
|
# From source
|
|
46
47
|
git clone https://github.com/devinoldenburg/opencode-goal-mode
|
|
47
48
|
cd opencode-goal-mode && npm ci && npm run install:global
|
|
@@ -49,16 +50,23 @@ cd opencode-goal-mode && npm ci && npm run install:global
|
|
|
49
50
|
|
|
50
51
|
Use global install for normal daily use. Use project install only when you want
|
|
51
52
|
Goal Mode scoped to one repo and your OpenCode build reads project `.opencode`
|
|
52
|
-
config, including `.opencode/tui.json`.
|
|
53
|
-
`--uninstall` removes only what it installed (and its `tui.json` entry), leaving
|
|
54
|
-
your edits untouched. See [Installer options](#installer-options).
|
|
53
|
+
config, including `.opencode/tui.json`. See [Installer options](#installer-options).
|
|
55
54
|
</details>
|
|
56
55
|
|
|
56
|
+
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
57
|
+
[](https://www.npmjs.com/package/opencode-goal-mode)
|
|
58
|
+
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/ci.yml)
|
|
59
|
+
[](https://github.com/devinoldenburg/opencode-goal-mode/actions/workflows/publish.yml)
|
|
60
|
+
[](LICENSE)
|
|
61
|
+
[](package.json)
|
|
62
|
+
|
|
57
63
|

|
|
58
64
|
|
|
59
|
-
<sub>↑ In
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
<sub>↑ In goal mode, the Goal plugin takes over the sidebar todo section with a
|
|
66
|
+
structured, evidence-aware Goal todo list — a bold `GOAL` label, then the goal
|
|
67
|
+
title, gate progress, and per-acceptance/gate todo rows, each on its own line in
|
|
68
|
+
its own colour, with a first-display rainbow. Build and every other mode keep
|
|
69
|
+
OpenCode's native todo section — see [TUI integration](#tui-integration).</sub>
|
|
62
70
|
|
|
63
71
|
**[Quick start](#quick-start) · [Why it's different](#why-its-different) · [Benchmarks](#benchmarks-honest-edition) · [TUI integration](#tui-integration) · [Configuration](#configuration) · [Releasing](#releasing) · [Architecture](ARCHITECTURE.md)**
|
|
64
72
|
|
|
@@ -218,9 +226,10 @@ second) — negligible for a per-tool-call guard:
|
|
|
218
226
|
reviewer's friendly name, and a single "completion unlocked" toast the moment
|
|
219
227
|
the last required gate clears.
|
|
220
228
|
- An **experimental** companion TUI plugin (`plugins/goal-sidebar.tsx`) that, in
|
|
221
|
-
Goal sessions only,
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
Goal sessions only, takes over the sidebar todo area with a structured,
|
|
230
|
+
evidence-aware Goal todo list (`GOAL` label, goal title, gate progress, and
|
|
231
|
+
todo rows — each on its own line in its own colour). It shows a first-display
|
|
232
|
+
rainbow, then normal goal colours. See [TUI integration](#tui-integration).
|
|
224
233
|
- A test suite validating the analyzer, plugin hooks, state store, install
|
|
225
234
|
safety, and config compatibility.
|
|
226
235
|
|
|
@@ -230,18 +239,29 @@ Goal Mode is a **plugin pair**: the server-side `goal-guard` plugin owns
|
|
|
230
239
|
enforcement and writes its state to disk, and an experimental TUI plugin
|
|
231
240
|
(`plugins/goal-sidebar.tsx`) reads that same state to render a live todo section.
|
|
232
241
|
|
|
233
|
-
- **Goal-owned todo section.** In a `goal` session
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
-
|
|
240
|
-
|
|
241
|
-
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
- **Goal-owned todo section.** In a `goal` session with a goal set, the Goal
|
|
243
|
+
plugin renders its own structured todo section into the sidebar's `sidebar_content`
|
|
244
|
+
slot, stacked on separate lines, each in its own colour so it never reads as one
|
|
245
|
+
run of text:
|
|
246
|
+
- a bold **`GOAL`** label (yellow while running, red when done);
|
|
247
|
+
- the short goal title;
|
|
248
|
+
- a `passing/total gates · status` line (lifecycle only — no "changes pending"
|
|
249
|
+
noise; pending work shows as a todo row instead);
|
|
250
|
+
- structured todo rows derived from real guard state: one per acceptance
|
|
251
|
+
criterion (✓ when fresh evidence covers it), a re-verify row when the tree
|
|
252
|
+
changed, and one row per still-missing review gate by friendly name
|
|
253
|
+
(e.g. "Pass Security Reviewer").
|
|
254
|
+
|
|
255
|
+
It opens with a first-display rainbow (`sidebarRainbowMs`) so the takeover is
|
|
256
|
+
visible, then settles to the lifecycle colours (running → yellow label; done →
|
|
257
|
+
red). Because OpenCode renders the native todo list as that slot's *fallback*,
|
|
258
|
+
on builds that render `sidebar_content` in replace/single-winner mode the Goal
|
|
259
|
+
section **replaces** the native todo list while a goal is active; in append mode
|
|
260
|
+
it sits alongside it. In every case:
|
|
261
|
+
- **no render** — Build and every non-Goal mode (and a Goal session before a
|
|
262
|
+
goal is set) render nothing here, so OpenCode's native todo section stays in
|
|
263
|
+
the same position. The section is scoped to the session that owns the goal: a
|
|
264
|
+
Build session in the same worktree never inherits another session's goal.
|
|
245
265
|
|
|
246
266
|
Toggle/recolour with `sidebarBanner`, `sidebarColor` (running), `sidebarDoneColor`
|
|
247
267
|
(done), `sidebarMutedColor`, `sidebarRainbowMs`, or the `GOAL_GUARD_SIDEBAR_*`
|
|
@@ -258,12 +278,18 @@ enforcement and writes its state to disk, and an experimental TUI plugin
|
|
|
258
278
|
{ "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }
|
|
259
279
|
```
|
|
260
280
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
281
|
+
OpenCode installs the referenced package into its own plugin cache
|
|
282
|
+
(`~/.cache/opencode/packages/`) and provides the `@opentui/solid` + `solid-js`
|
|
283
|
+
runtime to it. It does **not** re-check that cache for newer versions, so the
|
|
284
|
+
installer clears the cached copy on install/uninstall — that's why an upgrade
|
|
285
|
+
needs only a restart to load the new sidebar. Restart OpenCode after install. The
|
|
286
|
+
Goal todo section appears in a **Goal session** view (not the home screen and not
|
|
287
|
+
Build mode), and because the Goal agent does its own todo tracking (native
|
|
288
|
+
`todowrite` is disabled in Goal Mode), it replaces — rather than sits beside —
|
|
289
|
+
the native todo list while a goal is active. The visual harness renders the
|
|
290
|
+
component headlessly in [visual test](tools/visual-test/README.md)
|
|
291
|
+
(`npm run test:visual`); the enforcement core is a separate server plugin and
|
|
292
|
+
works regardless of the sidebar.
|
|
267
293
|
- **Toasts.** Review verdicts and completion-unlock events surface as toasts
|
|
268
294
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
269
295
|
toast as before (`toastOnBlock`).
|
package/agents/goal.md
CHANGED
|
@@ -28,7 +28,7 @@ permission:
|
|
|
28
28
|
"/projects/**": allow
|
|
29
29
|
"~/.config/opencode/**": allow
|
|
30
30
|
"~/.local/share/opencode/tool-output/**": allow
|
|
31
|
-
todowrite:
|
|
31
|
+
todowrite: deny
|
|
32
32
|
question: allow
|
|
33
33
|
webfetch: allow
|
|
34
34
|
websearch: allow
|
|
@@ -103,7 +103,7 @@ Operating loop:
|
|
|
103
103
|
1. Establish the Goal Contract, constraints, current state, and acceptance criteria.
|
|
104
104
|
2. If essential information is missing, ask all necessary clarifying questions immediately at the beginning. Do not defer avoidable questions into the build phase.
|
|
105
105
|
3. Delegate research and discovery before editing. Use subagents to inspect local files, map structures, trace code paths, research docs, identify verification commands, and gather external web evidence.
|
|
106
|
-
4.
|
|
106
|
+
4. Track progress through the Goal Contract acceptance criteria and the guard's evidence/gate state, not the native todo tool. Goal Mode owns the sidebar todo section: it derives a live, structured todo list from the acceptance criteria (checked off as you record evidence), dirty state, and outstanding review gates. Do not use `todowrite` (it is disabled in Goal Mode so the native todo list never competes with the Goal-owned section); call `goal_status`/`goal_evidence_map` when you need the current checklist.
|
|
107
107
|
5. Implement the goal yourself in the main agent unless a bounded implementation subtask is explicitly safer to delegate.
|
|
108
108
|
6. Run or delegate relevant checks, tests, builds, linters, typechecks, previews, or manual verification planning.
|
|
109
109
|
7. When you believe the goal is finished, immediately run a strict review cycle before telling the user. The review must compare the original prompt and Goal Contract against the actual result.
|
package/docs/sidebar-demo.svg
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="452" viewBox="0 0 1000 452" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" role="img" aria-label="OpenCode Goal Mode sidebar: in a goal session the plugin renders a stacked Goal todo section — a bold GOAL label, the goal title, a gates/status line, and per-gate todo rows — each on its own line in its own colour, opening with a first-display rainbow then settling (GOAL yellow, title white, status cyan) and turning red when done. Build and other modes render no Goal section and keep OpenCode's native todo list.">
|
|
2
2
|
<defs>
|
|
3
3
|
<style>
|
|
4
4
|
.win { fill: #0d1117; stroke: #30363d; stroke-width: 1; }
|
|
5
5
|
.bar { fill: #161b22; }
|
|
6
6
|
.dot { stroke-width: 0; }
|
|
7
7
|
.title { fill: #8b949e; font-size: 12px; }
|
|
8
|
-
.
|
|
9
|
-
.lbl
|
|
10
|
-
.ln
|
|
11
|
-
.sm
|
|
8
|
+
.hdr { fill: #8b949e; font-size: 11px; letter-spacing: 0.5px; }
|
|
9
|
+
.lbl { font-size: 13px; font-weight: 700; }
|
|
10
|
+
.ln { font-size: 13px; }
|
|
11
|
+
.sm { font-size: 12px; }
|
|
12
12
|
.chat { fill: #c9d1d9; font-size: 12px; }
|
|
13
13
|
.dim { fill: #6e7681; font-size: 12px; }
|
|
14
14
|
.ok { fill: #2da44e; font-size: 12px; }
|
|
15
|
-
.nat { fill: #8b949e; font-size: 12px; }
|
|
16
15
|
.div { stroke: #30363d; stroke-width: 1; }
|
|
17
|
-
/* first-display rainbow
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
16
|
+
/* first-display rainbow — successive hues per line (RAINBOW[index]) */
|
|
17
|
+
.r0 { fill: #FF5555; } .r1 { fill: #FFAA00; } .r2 { fill: #FFFF55; } .r3 { fill: #55FF55; }
|
|
18
|
+
/* settled running palette */
|
|
19
|
+
.label { fill: #FFD700; } /* GOAL — yellow */
|
|
20
|
+
.gtitle { fill: #FFFFFF; } /* goal title — bright white */
|
|
21
|
+
.meta { fill: #8BE9FD; } /* gates · status — cyan */
|
|
22
|
+
.todo { fill: #808080; } /* pending todo rows — grey */
|
|
23
|
+
.done { fill: #FF5555; } /* done state — red */
|
|
24
|
+
.check { fill: #50FA7B; } /* ✓ done rows — green */
|
|
21
25
|
</style>
|
|
22
26
|
</defs>
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
<rect class="win" x="1" y="1" width="998" height="358" rx="8"/>
|
|
28
|
+
<rect class="win" x="1" y="1" width="998" height="450" rx="8"/>
|
|
26
29
|
<rect class="bar" x="1" y="1" width="998" height="30" rx="8"/>
|
|
27
30
|
<rect class="bar" x="1" y="20" width="998" height="11"/>
|
|
28
31
|
<circle class="dot" cx="20" cy="16" r="5" fill="#ff5f56"/>
|
|
@@ -30,8 +33,7 @@
|
|
|
30
33
|
<circle class="dot" cx="56" cy="16" r="5" fill="#27c93f"/>
|
|
31
34
|
<text class="title" x="86" y="20">opencode — goal mode</text>
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
<line class="div" x1="470" y1="31" x2="470" y2="359"/>
|
|
36
|
+
<line class="div" x1="470" y1="31" x2="470" y2="451"/>
|
|
35
37
|
|
|
36
38
|
<!-- chat pane (left) -->
|
|
37
39
|
<text class="dim" x="20" y="58">▌ goal</text>
|
|
@@ -40,30 +42,37 @@
|
|
|
40
42
|
<text class="ok" x="20" y="126">✓ Security Reviewer → PASS</text>
|
|
41
43
|
<text class="ok" x="20" y="148">✓ Verifier → PASS</text>
|
|
42
44
|
<text class="dim" x="20" y="170">▸ Diff Reviewer running…</text>
|
|
43
|
-
<text class="dim" x="20" y="
|
|
44
|
-
<text class="dim" x="20" y="
|
|
45
|
-
<text class="dim" x="20" y="
|
|
45
|
+
<text class="dim" x="20" y="212">Build / Plan / custom agents cannot</text>
|
|
46
|
+
<text class="dim" x="20" y="230">invoke the goal-* reviewers, and never</text>
|
|
47
|
+
<text class="dim" x="20" y="248">get a Goal section in their sidebar.</text>
|
|
46
48
|
|
|
47
|
-
<!--
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
<text
|
|
52
|
-
<text class="sm
|
|
53
|
-
<text class="sm
|
|
54
|
-
<text class="sm r2" x="490" y="138">goal-reviewer, goal-diff-reviewer…</text>
|
|
55
|
-
<text class="dim" x="490" y="156">↳ then settles to yellow while running</text>
|
|
49
|
+
<!-- Panel 1: running, first-display rainbow -->
|
|
50
|
+
<text class="hdr" x="490" y="52">GOAL SESSION · running · first-display rainbow</text>
|
|
51
|
+
<line class="div" x1="490" y1="60" x2="980" y2="60"/>
|
|
52
|
+
<text class="lbl r0" x="490" y="80">GOAL</text>
|
|
53
|
+
<text class="ln r1" x="490" y="98">Ship the OAuth refactor</text>
|
|
54
|
+
<text class="sm r2" x="490" y="116">0/5 gates · in progress</text>
|
|
55
|
+
<text class="sm r3" x="490" y="134">□ Pass Prompt Auditor</text>
|
|
56
56
|
|
|
57
|
-
<!--
|
|
58
|
-
<text class="
|
|
59
|
-
<line class="div" x1="490" y1="
|
|
60
|
-
<text
|
|
61
|
-
<text class="
|
|
62
|
-
<text class="sm
|
|
57
|
+
<!-- Panel 2: running, settled (distinct colours per line) -->
|
|
58
|
+
<text class="hdr" x="490" y="166">GOAL SESSION · running · settled colours</text>
|
|
59
|
+
<line class="div" x1="490" y1="174" x2="980" y2="174"/>
|
|
60
|
+
<text class="lbl label" x="490" y="194">GOAL</text>
|
|
61
|
+
<text class="ln gtitle" x="490" y="212">Ship the OAuth refactor</text>
|
|
62
|
+
<text class="sm meta" x="490" y="230">3/5 gates · in progress</text>
|
|
63
|
+
<text class="sm todo" x="490" y="248">□ Pass Security Reviewer</text>
|
|
63
64
|
|
|
64
|
-
<!--
|
|
65
|
-
<text class="
|
|
66
|
-
<line class="div" x1="490" y1="
|
|
67
|
-
<text class="
|
|
68
|
-
<text class="
|
|
65
|
+
<!-- Panel 3: done (red) -->
|
|
66
|
+
<text class="hdr" x="490" y="280">GOAL SESSION · done</text>
|
|
67
|
+
<line class="div" x1="490" y1="288" x2="980" y2="288"/>
|
|
68
|
+
<text class="lbl done" x="490" y="308">GOAL</text>
|
|
69
|
+
<text class="ln done" x="490" y="326">Fix the parser bug</text>
|
|
70
|
+
<text class="sm done" x="490" y="344">5/5 gates · completed · 2 review cycles</text>
|
|
71
|
+
<text class="sm check" x="490" y="362">✓ All acceptance criteria covered</text>
|
|
72
|
+
|
|
73
|
+
<!-- Panel 4: build / other mode → native todos, no Goal section -->
|
|
74
|
+
<text class="hdr" x="490" y="394">BUILD / OTHER MODE</text>
|
|
75
|
+
<line class="div" x1="490" y1="402" x2="980" y2="402"/>
|
|
76
|
+
<text class="todo" x="490" y="422" font-size="12">▢ OpenCode's native todo section</text>
|
|
77
|
+
<text class="dim" x="490" y="440">(no Goal section is rendered here)</text>
|
|
69
78
|
</svg>
|
package/package.json
CHANGED
|
@@ -178,6 +178,13 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
178
178
|
async "tool.execute.after"(inp, out) {
|
|
179
179
|
try {
|
|
180
180
|
const state = store.stateFor(inp?.sessionID);
|
|
181
|
+
// Goal bookkeeping is GOAL-ONLY. A Build/Plan/custom session must never have
|
|
182
|
+
// its edits, mutations, verification, or verdicts recorded as goal state —
|
|
183
|
+
// otherwise the guard would treat a non-Goal message/task as if it were a
|
|
184
|
+
// goal. Only an active Goal session (or a goal-namespace subagent's own
|
|
185
|
+
// session, for verdict capture) is tracked. Destructive-command blocking is
|
|
186
|
+
// handled in tool.execute.before and still applies in every mode.
|
|
187
|
+
if (!state.active && !isGoalAgent(state.currentAgent)) return;
|
|
181
188
|
const tool = inp?.tool;
|
|
182
189
|
const isReviewing = isReviewAgent(state.currentAgent);
|
|
183
190
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { requiredGates, missingGates, gatePassedFresh } from "./gates.js";
|
|
7
|
+
import { prettyAgentName } from "./agents.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A short, single-line label for the current goal.
|
|
@@ -35,33 +36,55 @@ function criterionEvidenceFresh(state, criterion) {
|
|
|
35
36
|
return entries.some((entry) => evidenceMatchesCriterion(entry, criterion) && evidenceFresh(entry, state));
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function clip(text, max) {
|
|
40
|
+
const s = String(text || "").replace(/\s+/g, " ").trim();
|
|
41
|
+
return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structured, ordered todo rows for the sidebar. Each row is a concrete, checkable
|
|
46
|
+
* step toward Goal completion — acceptance criteria first (✓ when fresh evidence
|
|
47
|
+
* covers them), then a re-verify nudge if the tree changed, then ONE row per still-
|
|
48
|
+
* missing review gate (friendly name, e.g. "Pass Security Reviewer"). This is the
|
|
49
|
+
* Goal plugin's own todo system: it is derived from real guard state (contract,
|
|
50
|
+
* evidence freshness, dirty flag, required gates), not transcript memory, so it
|
|
51
|
+
* tracks completion truthfully and updates as gates clear.
|
|
52
|
+
*/
|
|
38
53
|
function sidebarTodos(state, required, missing) {
|
|
39
54
|
const criteria = Array.isArray(state?.contract?.acceptanceCriteria) ? state.contract.acceptanceCriteria : [];
|
|
40
55
|
const items = [];
|
|
41
|
-
for (const criterion of criteria.slice(0,
|
|
42
|
-
const text =
|
|
56
|
+
for (const criterion of criteria.slice(0, 4)) {
|
|
57
|
+
const text = clip(criterion, 52);
|
|
43
58
|
if (!text) continue;
|
|
44
|
-
items.push({
|
|
45
|
-
status: criterionEvidenceFresh(state, text) ? "done" : "todo",
|
|
46
|
-
text: text.length <= 58 ? text : `${text.slice(0, 57).trimEnd()}…`,
|
|
47
|
-
});
|
|
59
|
+
items.push({ status: criterionEvidenceFresh(state, text) ? "done" : "todo", text });
|
|
48
60
|
}
|
|
49
|
-
if (state?.dirty) items.push({ status: "todo", text: "
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
if (state?.dirty) items.push({ status: "todo", text: "Re-verify & re-review after recent edits" });
|
|
62
|
+
// One row per missing/stale review gate, by friendly name — more scannable than a
|
|
63
|
+
// comma-joined id list and it shows exactly which reviewer is still owed.
|
|
64
|
+
for (const gate of missing.slice(0, 4)) {
|
|
65
|
+
items.push({ status: "todo", text: `Pass ${prettyAgentName(gate)}` });
|
|
66
|
+
}
|
|
67
|
+
if (items.length === 0 && required.length > 0) {
|
|
68
|
+
items.push({ status: "todo", text: "Record the Goal Contract & acceptance criteria" });
|
|
69
|
+
}
|
|
70
|
+
return items.slice(0, 8);
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
/**
|
|
56
74
|
* Compact projection for the TUI sidebar todo section. ALWAYS returns an object with a
|
|
57
|
-
* three-way `state`, plus
|
|
58
|
-
*
|
|
59
|
-
* - `
|
|
60
|
-
* - `
|
|
61
|
-
*
|
|
75
|
+
* three-way `state`, plus lines that stack vertically in the sidebar — each rendered
|
|
76
|
+
* on its OWN line in a distinct colour so it never reads as one run of text:
|
|
77
|
+
* - `label` → line 1: the fixed "GOAL" header (yellow when running, red when done).
|
|
78
|
+
* - `goal` → line 2: the short AI goal title.
|
|
79
|
+
* - `gates` → line 3: gate count + lifecycle, e.g. "3/5 gates · in progress" or
|
|
80
|
+
* "5/5 gates · completed · 2 review cycles". No "changes pending"
|
|
81
|
+
* noise — pending work surfaces as a structured todo row instead.
|
|
82
|
+
* - `todos` → following lines: structured acceptance/verification/gate todos.
|
|
62
83
|
* State drives colour: "running" = rainbow first, then yellow; "done" = red;
|
|
63
|
-
* "none" = render nothing so non-Goal
|
|
84
|
+
* "none" = render nothing so non-Goal / no-goal sessions keep the native todo section.
|
|
64
85
|
*/
|
|
86
|
+
export const GOAL_LABEL = "GOAL";
|
|
87
|
+
|
|
65
88
|
export function sidebarView(state, config) {
|
|
66
89
|
if (!state || !state.active) return NO_GOAL;
|
|
67
90
|
const goal = shortGoalLabel(state);
|
|
@@ -76,10 +99,11 @@ export function sidebarView(state, config) {
|
|
|
76
99
|
if (done) {
|
|
77
100
|
return {
|
|
78
101
|
state: "done",
|
|
102
|
+
label: GOAL_LABEL,
|
|
79
103
|
goal,
|
|
80
104
|
gates,
|
|
81
105
|
status: `completed · ${cycles} review cycle${cycles === 1 ? "" : "s"}`,
|
|
82
|
-
todoTitle:
|
|
106
|
+
todoTitle: GOAL_LABEL,
|
|
83
107
|
todos: todos.length ? todos.map((item) => ({ ...item, status: "done" })) : [{ status: "done", text: "All Goal completion gates are clear" }],
|
|
84
108
|
passing,
|
|
85
109
|
required: required.length,
|
|
@@ -88,10 +112,11 @@ export function sidebarView(state, config) {
|
|
|
88
112
|
}
|
|
89
113
|
return {
|
|
90
114
|
state: "running",
|
|
115
|
+
label: GOAL_LABEL,
|
|
91
116
|
goal,
|
|
92
117
|
gates,
|
|
93
|
-
status:
|
|
94
|
-
todoTitle:
|
|
118
|
+
status: "in progress",
|
|
119
|
+
todoTitle: GOAL_LABEL,
|
|
95
120
|
todos,
|
|
96
121
|
passing,
|
|
97
122
|
required: required.length,
|
package/plugins/goal-sidebar.tsx
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Goal Mode — TUI sidebar todo section.
|
|
4
4
|
*
|
|
5
|
-
* In Goal agent sessions this
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* In Goal agent sessions this renders a Goal-owned, evidence-aware todo section
|
|
6
|
+
* into the sidebar_content slot (GOAL label, goal title, gate/status line, and
|
|
7
|
+
* structured todo rows). OpenCode renders the native todo list as that slot's
|
|
8
|
+
* fallback, so in replace/single-winner slot mode this REPLACES the native todos
|
|
9
|
+
* while a goal is active; rendering nothing (non-Goal or no-goal sessions) brings
|
|
10
|
+
* the native todos back. The section is strictly per-session: it is keyed by
|
|
11
|
+
* props.session_id, so a Build session in the same worktree never inherits
|
|
12
|
+
* another session's goal.
|
|
10
13
|
*
|
|
11
14
|
* How OpenCode loads this: TUI plugins are listed in `~/.config/opencode/tui.json`
|
|
12
15
|
* (NOT the plugins/ dir) and resolved via the package's `exports["./tui"]`. The
|
|
@@ -20,13 +23,17 @@
|
|
|
20
23
|
* the Node test suite.
|
|
21
24
|
*/
|
|
22
25
|
|
|
23
|
-
import { createSignal, onCleanup, For, Show } from "solid-js";
|
|
26
|
+
import { createSignal, createEffect, onCleanup, For, Show } from "solid-js";
|
|
24
27
|
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
25
28
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
26
29
|
|
|
27
|
-
const DEFAULT_COLOR = "#FFD700"; // running — yellow
|
|
30
|
+
const DEFAULT_COLOR = "#FFD700"; // running — GOAL label, yellow
|
|
28
31
|
const DEFAULT_DONE = "#FF5555"; // done — red
|
|
29
|
-
const DEFAULT_MUTED = "#808080"; //
|
|
32
|
+
const DEFAULT_MUTED = "#808080"; // pending todo rows — grey
|
|
33
|
+
const TITLE_COLOR = "#FFFFFF"; // goal title line (running) — bright, distinct from the yellow GOAL label
|
|
34
|
+
const META_COLOR = "#8BE9FD"; // gates line (running) — cyan accent
|
|
35
|
+
const STATUS_COLOR = "#FFB86C"; // status line (running) — orange, distinct from the cyan gates line
|
|
36
|
+
const TODO_DONE_COLOR = "#50FA7B"; // ✓ done todo rows — green
|
|
30
37
|
const POLL_MS = 1500;
|
|
31
38
|
const RAINBOW = ["#FF5555", "#FFAA00", "#FFFF55", "#55FF55", "#55FFFF", "#5599FF", "#FF55FF"];
|
|
32
39
|
|
|
@@ -79,16 +86,27 @@ function pickSession(snapshot, sessionId) {
|
|
|
79
86
|
return null;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the sidebar model for a session, trying each candidate worktree key in
|
|
91
|
+
* turn. The guard persists keyed by `worktree || directory`; the TUI may surface
|
|
92
|
+
* either path, so we try both (worktree first) rather than risk a key mismatch
|
|
93
|
+
* that would hide an active goal and leave the native todos showing.
|
|
94
|
+
*/
|
|
95
|
+
function readModel(worktrees, sessionId) {
|
|
96
|
+
const keys = (Array.isArray(worktrees) ? worktrees : [worktrees]).filter(Boolean);
|
|
97
|
+
for (const wt of keys) {
|
|
98
|
+
try {
|
|
99
|
+
const snapshot = readSnapshot(wt);
|
|
100
|
+
if (!snapshot) continue;
|
|
101
|
+
const record = pickSession(snapshot, sessionId);
|
|
102
|
+
if (!record) continue;
|
|
103
|
+
const view = sidebarView(record, DEFAULT_CONFIG);
|
|
104
|
+
if (view && view.state !== "none") return view;
|
|
105
|
+
} catch {
|
|
106
|
+
/* try the next candidate */
|
|
107
|
+
}
|
|
91
108
|
}
|
|
109
|
+
return NO_GOAL;
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
const id = "goal-mode-sidebar";
|
|
@@ -96,11 +114,13 @@ const id = "goal-mode-sidebar";
|
|
|
96
114
|
/** @type {import("@opencode-ai/plugin/tui").TuiPlugin} */
|
|
97
115
|
const tui = async (api, options) => {
|
|
98
116
|
try {
|
|
99
|
-
const { enabled, color, doneColor, rainbowMs } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
117
|
+
const { enabled, color, doneColor, muted, rainbowMs } = resolveOptions(options, typeof process !== "undefined" ? process.env : {});
|
|
100
118
|
if (!enabled) return;
|
|
101
119
|
if (!api?.slots?.register) return; // runtime without the slot API → no-op.
|
|
102
120
|
|
|
103
|
-
|
|
121
|
+
// The guard keys persisted state by worktree (falling back to directory).
|
|
122
|
+
// Surface both so a path-key mismatch can't hide an active goal.
|
|
123
|
+
const worktrees = [api.state?.path?.worktree, api.state?.path?.directory];
|
|
104
124
|
|
|
105
125
|
api.slots.register({
|
|
106
126
|
order: 50,
|
|
@@ -109,32 +129,69 @@ const tui = async (api, options) => {
|
|
|
109
129
|
if (!props?.session_id) return undefined;
|
|
110
130
|
const read = () => {
|
|
111
131
|
try {
|
|
112
|
-
return readModel(
|
|
132
|
+
return readModel(worktrees, props?.session_id) || NO_GOAL;
|
|
113
133
|
} catch {
|
|
114
134
|
return NO_GOAL;
|
|
115
135
|
}
|
|
116
136
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
// ALWAYS mount a reactive, polling component — do NOT bail when there is
|
|
138
|
+
// no goal yet. The goal is normally recorded AFTER the sidebar mounts
|
|
139
|
+
// (the user opens the session, then states the goal), so the slot must
|
|
140
|
+
// keep polling and let <Show> reveal the section when the goal appears.
|
|
141
|
+
// Returning undefined at mount (the old behavior) meant the poll never
|
|
142
|
+
// ran and the Goal section never showed even once a goal existed.
|
|
143
|
+
const first = read();
|
|
144
|
+
const [model, setModel] = createSignal(first);
|
|
121
145
|
const timer = setInterval(() => setModel(read()), POLL_MS);
|
|
122
|
-
const rainbowTimer = setTimeout(() => setRainbow(false), Math.max(0, rainbowMs || 0));
|
|
123
146
|
onCleanup(() => clearInterval(timer));
|
|
147
|
+
// First-display rainbow: starts the moment a goal FIRST appears. If a goal
|
|
148
|
+
// is already present at mount it starts immediately; otherwise the effect
|
|
149
|
+
// fires when the goal later appears (the common case — the goal is set
|
|
150
|
+
// after the sidebar mounts). Either way it settles after rainbowMs.
|
|
151
|
+
const [rainbow, setRainbow] = createSignal(false);
|
|
152
|
+
let rainbowStarted = false;
|
|
153
|
+
let rainbowTimer;
|
|
154
|
+
const startRainbow = () => {
|
|
155
|
+
if (rainbowStarted || (rainbowMs || 0) <= 0) return;
|
|
156
|
+
rainbowStarted = true;
|
|
157
|
+
setRainbow(true);
|
|
158
|
+
rainbowTimer = setTimeout(() => setRainbow(false), Math.max(0, rainbowMs));
|
|
159
|
+
};
|
|
160
|
+
if (first.state !== "none") startRainbow();
|
|
161
|
+
createEffect(() => {
|
|
162
|
+
if (model().state !== "none") startRainbow();
|
|
163
|
+
});
|
|
124
164
|
onCleanup(() => clearTimeout(rainbowTimer));
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
//
|
|
165
|
+
const isRainbow = () => rainbow() && model().state === "running";
|
|
166
|
+
// Settled (post-rainbow) colour for each header line. When done, every
|
|
167
|
+
// line is red; while running each line gets its OWN highlight colour so
|
|
168
|
+
// the GOAL label, the goal title, and the status never read as one text.
|
|
169
|
+
const settled = (kind) => {
|
|
170
|
+
if (model().state === "done") return doneColor;
|
|
171
|
+
if (kind === "label") return color; // GOAL — yellow
|
|
172
|
+
if (kind === "title") return TITLE_COLOR; // goal title — bright white
|
|
173
|
+
if (kind === "gates") return META_COLOR; // gate count — cyan
|
|
174
|
+
return STATUS_COLOR; // lifecycle status — orange
|
|
175
|
+
};
|
|
176
|
+
const lineColor = (index, kind) => (isRainbow() ? RAINBOW[index % RAINBOW.length] : settled(kind));
|
|
177
|
+
const todoColor = (index, item) => {
|
|
178
|
+
if (isRainbow()) return RAINBOW[index % RAINBOW.length];
|
|
179
|
+
if (item.status === "done") return TODO_DONE_COLOR;
|
|
180
|
+
return model().state === "done" ? doneColor : muted;
|
|
181
|
+
};
|
|
182
|
+
// Goal sessions render a Goal-owned todo section — GOAL label, goal title,
|
|
183
|
+
// gate count, lifecycle status, then structured todos — EACH on its own
|
|
184
|
+
// line in its own colour. Non-Goal / no-goal sessions returned undefined
|
|
185
|
+
// above, so the native todo section shows instead.
|
|
128
186
|
return (
|
|
129
187
|
<Show when={model().state !== "none"}>
|
|
130
188
|
<box flexDirection="column" paddingTop={1}>
|
|
131
|
-
<text fg={lineColor(0)}>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</text>
|
|
135
|
-
<text fg={lineColor(1)}>{`${model().gates} · ${model().status}`}</text>
|
|
189
|
+
<text fg={lineColor(0, "label")}><b>{model().label || "GOAL"}</b></text>
|
|
190
|
+
<text fg={lineColor(1, "title")}>{model().goal}</text>
|
|
191
|
+
<text fg={lineColor(2, "gates")}>{model().gates}</text>
|
|
192
|
+
<text fg={lineColor(3, "status")}>{model().status}</text>
|
|
136
193
|
<For each={model().todos || []}>
|
|
137
|
-
{(item, index) => <text fg={
|
|
194
|
+
{(item, index) => <text fg={todoColor(index() + 4, item)}>{`${item.status === "done" ? "✓" : "□"} ${item.text}`}</text>}
|
|
138
195
|
</For>
|
|
139
196
|
</box>
|
|
140
197
|
</Show>
|
package/scripts/install.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { join, resolve, relative, dirname } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
|
+
import { homedir } from "node:os";
|
|
18
19
|
import { parseArgs } from "node:util";
|
|
19
20
|
|
|
20
21
|
const { values } = parseArgs({
|
|
@@ -67,8 +68,18 @@ if (values.global && values.target) {
|
|
|
67
68
|
function resolveTarget() {
|
|
68
69
|
if (values.target) return resolve(String(values.target));
|
|
69
70
|
if (values.global) {
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// OpenCode reads global config from ~/.config/opencode. Resolve home from $HOME,
|
|
72
|
+
// falling back to the OS home dir (homedir() works where $HOME is unset, e.g.
|
|
73
|
+
// some CI/container shells) so --global doesn't fail in those environments.
|
|
74
|
+
let home = process.env.HOME;
|
|
75
|
+
if (!home) {
|
|
76
|
+
try {
|
|
77
|
+
home = homedir();
|
|
78
|
+
} catch {
|
|
79
|
+
home = "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!home) throw new Error("Cannot resolve a home directory for --global install. Pass --target <dir> instead.");
|
|
72
83
|
return join(home, ".config", "opencode");
|
|
73
84
|
}
|
|
74
85
|
return resolve(process.cwd(), ".opencode");
|
|
@@ -172,6 +183,35 @@ function ensureTuiPlugin(remove = false) {
|
|
|
172
183
|
return true;
|
|
173
184
|
}
|
|
174
185
|
|
|
186
|
+
/**
|
|
187
|
+
* OpenCode caches TUI plugins under `~/.cache/opencode/packages/<name>@<spec>/`
|
|
188
|
+
* and does NOT re-check npm for a newer version, so after an upgrade it keeps
|
|
189
|
+
* loading the OLD sidebar build. Removing our cache entries forces OpenCode to
|
|
190
|
+
* re-fetch the just-installed version on its next start. Returns the dirs cleared.
|
|
191
|
+
*/
|
|
192
|
+
function refreshTuiPluginCache() {
|
|
193
|
+
const base = (process.env.XDG_CACHE_HOME && process.env.XDG_CACHE_HOME.trim()) || join(homedir() || "", ".cache");
|
|
194
|
+
const pkgRoot = join(base, "opencode", "packages");
|
|
195
|
+
if (!existsSync(pkgRoot)) return [];
|
|
196
|
+
let entries;
|
|
197
|
+
try {
|
|
198
|
+
entries = readdirSync(pkgRoot);
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const ours = entries
|
|
203
|
+
.filter((name) => name === pkg.name || name.startsWith(`${pkg.name}@`))
|
|
204
|
+
.map((name) => join(pkgRoot, name));
|
|
205
|
+
for (const dir of ours) {
|
|
206
|
+
try {
|
|
207
|
+
if (!values["dry-run"]) rmSync(dir, { recursive: true, force: true });
|
|
208
|
+
} catch {
|
|
209
|
+
/* best-effort */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return ours;
|
|
213
|
+
}
|
|
214
|
+
|
|
175
215
|
// ---------------------------------------------------------------------------
|
|
176
216
|
// Uninstall
|
|
177
217
|
// ---------------------------------------------------------------------------
|
|
@@ -194,6 +234,8 @@ if (values.uninstall) {
|
|
|
194
234
|
const tuiRemoved = ensureTuiPlugin(true);
|
|
195
235
|
if (!values["dry-run"]) pruneEmptyDirs(target, Object.keys(manifest.files));
|
|
196
236
|
if (tuiRemoved) console.log(`${values["dry-run"] ? "Would remove" : "Removed"} the sidebar entry from ${join(target, "tui.json")}`);
|
|
237
|
+
const cachedCleared = refreshTuiPluginCache();
|
|
238
|
+
if (cachedCleared.length) console.log(`${values["dry-run"] ? "Would clear" : "Cleared"} OpenCode's cached TUI plugin (${cachedCleared.length}).`);
|
|
197
239
|
const verb = values["dry-run"] ? "Would remove" : "Removed";
|
|
198
240
|
console.log(`${verb} ${removed.length} Goal Mode files from ${target}.`);
|
|
199
241
|
if (kept.length) {
|
|
@@ -291,4 +333,8 @@ console.log(
|
|
|
291
333
|
`Files copied: ${summary.copied.length}; unchanged: ${summary.unchanged.length}; pruned: ${summary.pruned.length}`,
|
|
292
334
|
);
|
|
293
335
|
if (tuiAdded) console.log(`Registered the experimental sidebar in ${join(target, "tui.json")}`);
|
|
336
|
+
const cacheCleared = refreshTuiPluginCache();
|
|
337
|
+
if (cacheCleared.length) {
|
|
338
|
+
console.log(`${values["dry-run"] ? "Would clear" : "Cleared"} OpenCode's stale TUI plugin cache so the sidebar reloads at the installed version.`);
|
|
339
|
+
}
|
|
294
340
|
console.log("Restart OpenCode for agents, commands, and plugins to load.");
|