opencode-goal-mode 0.4.1 → 0.4.3
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 +66 -0
- package/README.md +17 -9
- package/agents/goal.md +2 -2
- package/package.json +4 -2
- package/plugins/goal-guard/gates.js +1 -1
- package/plugins/goal-guard/guard.js +17 -9
- package/plugins/goal-sidebar.tsx +87 -24
- package/scripts/install.mjs +35 -0
- package/scripts/postinstall.mjs +53 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.3
|
|
4
|
+
|
|
5
|
+
### Build mode no longer behaves like a goal
|
|
6
|
+
|
|
7
|
+
- Switching a session from the `goal` agent to Build (or any non-goal agent) now
|
|
8
|
+
**deactivates** it: `state.active` tracks the current agent (`isPrimaryAgent`),
|
|
9
|
+
instead of latching `true` forever. The sidebar also gates on the session's live
|
|
10
|
+
current agent (its latest message), so when you switch to Build the Goal section
|
|
11
|
+
disappears and OpenCode's native todos return — and a Build session can no longer
|
|
12
|
+
invoke the `goal-*` subagents or have its completion claims policed.
|
|
13
|
+
- Goal **worker** subagents (e.g. `goal-implementer`) are no longer activated by
|
|
14
|
+
their own edits, so Goal completion enforcement is never injected into a worker's
|
|
15
|
+
prompt. Bookkeeping runs only for active goal sessions and review subagents.
|
|
16
|
+
|
|
17
|
+
### `npm install -g` now updates everything, automatically
|
|
18
|
+
|
|
19
|
+
- A global-install `postinstall` runs the installer for you — it copies the
|
|
20
|
+
components into `~/.config/opencode`, registers the Goal sidebar, and clears
|
|
21
|
+
OpenCode's stale plugin cache — so `npm install -g opencode-goal-mode` **alone**
|
|
22
|
+
fully installs or upgrades Goal Mode and the new version actually loads on the
|
|
23
|
+
next restart. It runs only for global installs, never for repo/dev/dependency
|
|
24
|
+
installs, and never fails the npm install (it prints a hint if it can't finish).
|
|
25
|
+
|
|
26
|
+
### Fixes
|
|
27
|
+
|
|
28
|
+
- **CI green again:** the headless visual test no longer requires
|
|
29
|
+
`@opentui/solid/jsx-dev-runtime` (the CI visual job was failing).
|
|
30
|
+
- Hardened `gatePassedFresh` against partial/legacy snapshots so the TUI never
|
|
31
|
+
silently fails to render an active goal.
|
|
32
|
+
- Compaction context is only added for active goal sessions (no stray Goal state in
|
|
33
|
+
a Build session's compaction summary).
|
|
34
|
+
- Docs: corrected the sidebar description (separate gate/status lines; the label is
|
|
35
|
+
`GOAL`, not "Goal todos").
|
|
36
|
+
|
|
37
|
+
## v0.4.2
|
|
38
|
+
|
|
39
|
+
### The sidebar Goal section now actually renders in the live TUI
|
|
40
|
+
|
|
41
|
+
- **Root-cause fix.** The `sidebar_content` slot bailed at mount with
|
|
42
|
+
`return undefined` whenever no goal existed *yet* — but the goal is normally set
|
|
43
|
+
*after* the sidebar mounts, so the polling component never started and the Goal
|
|
44
|
+
section never appeared. The slot now **always mounts a reactive, polling
|
|
45
|
+
component** and reveals the section (via `<Show>`) the moment the goal is
|
|
46
|
+
recorded. Verified by driving the real OpenCode TUI in a PTY.
|
|
47
|
+
- **Stale plugin cache.** OpenCode caches TUI plugins under
|
|
48
|
+
`~/.cache/opencode/packages/<name>@<spec>/` and never re-checks npm, so upgrades
|
|
49
|
+
kept loading the *old* sidebar build. The installer now clears that cache on
|
|
50
|
+
install and uninstall, so a restart picks up the installed version.
|
|
51
|
+
- The first-display rainbow now triggers when the goal first appears (not at
|
|
52
|
+
mount, when there may be no goal yet).
|
|
53
|
+
- The sidebar resolves state under both the worktree and directory path keys, so a
|
|
54
|
+
path-key mismatch can't hide an active goal.
|
|
55
|
+
|
|
56
|
+
### Sidebar layout
|
|
57
|
+
|
|
58
|
+
- The gate count and the lifecycle status are now on **separate lines**, each in
|
|
59
|
+
its own colour (GOAL = yellow, title = white, gates = cyan, status = orange).
|
|
60
|
+
|
|
61
|
+
### Native todos are replaced in goal mode
|
|
62
|
+
|
|
63
|
+
- The `goal` agent no longer uses the native `todowrite` tool (it is disabled in
|
|
64
|
+
Goal Mode). Because OpenCode renders native todos as their own sidebar slot, the
|
|
65
|
+
only way to replace them is to stop producing them — so in a goal session the
|
|
66
|
+
native todo list stays empty and the structured Goal-owned section is what shows.
|
|
67
|
+
Build and every other mode keep their native todos.
|
|
68
|
+
|
|
3
69
|
## v0.4.1
|
|
4
70
|
|
|
5
71
|
### Restructured Goal sidebar todo section
|
package/README.md
CHANGED
|
@@ -244,9 +244,11 @@ enforcement and writes its state to disk, and an experimental TUI plugin
|
|
|
244
244
|
slot, stacked on separate lines, each in its own colour so it never reads as one
|
|
245
245
|
run of text:
|
|
246
246
|
- a bold **`GOAL`** label (yellow while running, red when done);
|
|
247
|
-
- the short goal title;
|
|
248
|
-
-
|
|
249
|
-
|
|
247
|
+
- the short goal title (white);
|
|
248
|
+
- the gate count `passing/total gates` (cyan), on its own line;
|
|
249
|
+
- the lifecycle status (orange) on its own line — `in progress`, or
|
|
250
|
+
`completed · N review cycles`. No "changes pending" noise; pending work shows
|
|
251
|
+
as a todo row instead;
|
|
250
252
|
- structured todo rows derived from real guard state: one per acceptance
|
|
251
253
|
criterion (✓ when fresh evidence covers it), a re-verify row when the tree
|
|
252
254
|
changed, and one row per still-missing review gate by friendly name
|
|
@@ -278,12 +280,18 @@ enforcement and writes its state to disk, and an experimental TUI plugin
|
|
|
278
280
|
{ "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }
|
|
279
281
|
```
|
|
280
282
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
OpenCode installs the referenced package into its own plugin cache
|
|
284
|
+
(`~/.cache/opencode/packages/`) and provides the `@opentui/solid` + `solid-js`
|
|
285
|
+
runtime to it. It does **not** re-check that cache for newer versions, so the
|
|
286
|
+
installer clears the cached copy on install/uninstall — that's why an upgrade
|
|
287
|
+
needs only a restart to load the new sidebar. Restart OpenCode after install. The
|
|
288
|
+
Goal todo section appears in a **Goal session** view (not the home screen and not
|
|
289
|
+
Build mode), and because the Goal agent does its own todo tracking (native
|
|
290
|
+
`todowrite` is disabled in Goal Mode), it replaces — rather than sits beside —
|
|
291
|
+
the native todo list while a goal is active. The visual harness renders the
|
|
292
|
+
component headlessly in [visual test](tools/visual-test/README.md)
|
|
293
|
+
(`npm run test:visual`); the enforcement core is a separate server plugin and
|
|
294
|
+
works regardless of the sidebar.
|
|
287
295
|
- **Toasts.** Review verdicts and completion-unlock events surface as toasts
|
|
288
296
|
(`toastOnReview`), and blocked destructive commands / premature completions
|
|
289
297
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-mode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "plugins/goal-sidebar.tsx",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"plugins/",
|
|
26
26
|
"research/",
|
|
27
27
|
"scripts/install.mjs",
|
|
28
|
+
"scripts/postinstall.mjs",
|
|
28
29
|
"ARCHITECTURE.md",
|
|
29
30
|
"CHANGELOG.md",
|
|
30
31
|
"LICENSE",
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"ci": "npm run validate && npm run audit",
|
|
52
53
|
"prepublishOnly": "npm run ci && npm run publish:check",
|
|
53
54
|
"install:local": "node scripts/install.mjs",
|
|
54
|
-
"install:global": "node scripts/install.mjs --global"
|
|
55
|
+
"install:global": "node scripts/install.mjs --global",
|
|
56
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
55
57
|
},
|
|
56
58
|
"keywords": [
|
|
57
59
|
"opencode",
|
|
@@ -71,7 +71,7 @@ export function requiredGates(state, config) {
|
|
|
71
71
|
|
|
72
72
|
/** A gate is satisfied when its latest verdict is PASS and newer than the last edit. */
|
|
73
73
|
export function gatePassedFresh(state, agent) {
|
|
74
|
-
const v = state.latestVerdict[agent];
|
|
74
|
+
const v = state.latestVerdict?.[agent];
|
|
75
75
|
if (!v || v.verdict !== "PASS") return false;
|
|
76
76
|
return v.seq > (state.lastEditSeq || 0);
|
|
77
77
|
}
|
|
@@ -93,7 +93,10 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
93
93
|
try {
|
|
94
94
|
if (!inp?.sessionID) return;
|
|
95
95
|
const state = store.stateFor(inp.sessionID);
|
|
96
|
-
|
|
96
|
+
// `active` reflects whether this session is CURRENTLY a Goal session. Switching
|
|
97
|
+
// the session's agent to Build (or anything non-goal) must deactivate it, or
|
|
98
|
+
// the sidebar/guard would keep treating an explicit Build session as a goal.
|
|
99
|
+
if (inp.agent) state.active = isPrimaryAgent(inp.agent);
|
|
97
100
|
const text = partsText(out?.parts);
|
|
98
101
|
if (text && state.active) {
|
|
99
102
|
// Accumulate goal text (bounded) so contextual gates can be derived.
|
|
@@ -115,7 +118,10 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
115
118
|
if (!normalized) return;
|
|
116
119
|
const state = store.stateFor(normalized);
|
|
117
120
|
state.currentAgent = inp.agent;
|
|
118
|
-
|
|
121
|
+
// Track the current mode: a session is active (a goal) only while its agent
|
|
122
|
+
// is the goal primary. Switching to Build/Plan/etc. deactivates it so the
|
|
123
|
+
// Goal sidebar and enforcement stop treating it as a goal.
|
|
124
|
+
if (inp.agent) state.active = isPrimaryAgent(inp.agent);
|
|
119
125
|
} catch {
|
|
120
126
|
/* ignore */
|
|
121
127
|
}
|
|
@@ -178,13 +184,14 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
178
184
|
async "tool.execute.after"(inp, out) {
|
|
179
185
|
try {
|
|
180
186
|
const state = store.stateFor(inp?.sessionID);
|
|
181
|
-
// Goal bookkeeping
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
187
|
+
// Goal bookkeeping runs only for an active Goal session, or for a REVIEW
|
|
188
|
+
// subagent's own child session (so the agent-path can capture its verdict).
|
|
189
|
+
// It must NOT run for a Build/Plan/custom session, nor for a non-review goal
|
|
190
|
+
// WORKER child session (e.g. goal-implementer/goal-explorer) — otherwise that
|
|
191
|
+
// worker's edits would mark it dirty and activate it, leaking goal completion
|
|
192
|
+
// enforcement into a worker's prompt. Destructive-command blocking lives in
|
|
193
|
+
// tool.execute.before and still applies in every mode.
|
|
194
|
+
if (!state.active && !isReviewAgent(state.currentAgent)) return;
|
|
188
195
|
const tool = inp?.tool;
|
|
189
196
|
const isReviewing = isReviewAgent(state.currentAgent);
|
|
190
197
|
|
|
@@ -278,6 +285,7 @@ export function createGuard(input = {}, options = {}, overrides = {}) {
|
|
|
278
285
|
try {
|
|
279
286
|
if (!inp?.sessionID || !out || !Array.isArray(out.context)) return;
|
|
280
287
|
const state = store.stateFor(inp.sessionID);
|
|
288
|
+
if (!state.active) return; // only preserve goal state for active Goal sessions
|
|
281
289
|
out.context.push(
|
|
282
290
|
`Goal Guard state: ${summarizeState(state, config)}. Preserve Goal Contract, Verification Ledger, ` +
|
|
283
291
|
`Review Ledger, Reviewer Memory, review cycle count, dirty state, and open findings across compaction.`,
|
package/plugins/goal-sidebar.tsx
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* the Node test suite.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { createSignal, onCleanup, For, Show } from "solid-js";
|
|
26
|
+
import { createSignal, createEffect, onCleanup, For, Show } from "solid-js";
|
|
27
27
|
import { sidebarView, NO_GOAL } from "./goal-guard/summary.js";
|
|
28
28
|
import { DEFAULT_CONFIG } from "./goal-guard/config.js";
|
|
29
29
|
|
|
@@ -31,9 +31,11 @@ const DEFAULT_COLOR = "#FFD700"; // running — GOAL label, yellow
|
|
|
31
31
|
const DEFAULT_DONE = "#FF5555"; // done — red
|
|
32
32
|
const DEFAULT_MUTED = "#808080"; // pending todo rows — grey
|
|
33
33
|
const TITLE_COLOR = "#FFFFFF"; // goal title line (running) — bright, distinct from the yellow GOAL label
|
|
34
|
-
const META_COLOR = "#8BE9FD"; // gates
|
|
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
|
|
35
36
|
const TODO_DONE_COLOR = "#50FA7B"; // ✓ done todo rows — green
|
|
36
37
|
const POLL_MS = 1500;
|
|
38
|
+
const GOAL_AGENT = "goal"; // the primary Goal agent id (mirrors agents.js PRIMARY_AGENT)
|
|
37
39
|
const RAINBOW = ["#FF5555", "#FFAA00", "#FFFF55", "#55FF55", "#55FFFF", "#5599FF", "#FF55FF"];
|
|
38
40
|
|
|
39
41
|
function resolveOptions(options, env) {
|
|
@@ -85,16 +87,27 @@ function pickSession(snapshot, sessionId) {
|
|
|
85
87
|
return null;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the sidebar model for a session, trying each candidate worktree key in
|
|
92
|
+
* turn. The guard persists keyed by `worktree || directory`; the TUI may surface
|
|
93
|
+
* either path, so we try both (worktree first) rather than risk a key mismatch
|
|
94
|
+
* that would hide an active goal and leave the native todos showing.
|
|
95
|
+
*/
|
|
96
|
+
function readModel(worktrees, sessionId) {
|
|
97
|
+
const keys = (Array.isArray(worktrees) ? worktrees : [worktrees]).filter(Boolean);
|
|
98
|
+
for (const wt of keys) {
|
|
99
|
+
try {
|
|
100
|
+
const snapshot = readSnapshot(wt);
|
|
101
|
+
if (!snapshot) continue;
|
|
102
|
+
const record = pickSession(snapshot, sessionId);
|
|
103
|
+
if (!record) continue;
|
|
104
|
+
const view = sidebarView(record, DEFAULT_CONFIG);
|
|
105
|
+
if (view && view.state !== "none") return view;
|
|
106
|
+
} catch {
|
|
107
|
+
/* try the next candidate */
|
|
108
|
+
}
|
|
97
109
|
}
|
|
110
|
+
return NO_GOAL;
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
const id = "goal-mode-sidebar";
|
|
@@ -106,27 +119,74 @@ const tui = async (api, options) => {
|
|
|
106
119
|
if (!enabled) return;
|
|
107
120
|
if (!api?.slots?.register) return; // runtime without the slot API → no-op.
|
|
108
121
|
|
|
109
|
-
|
|
122
|
+
// The guard keys persisted state by worktree (falling back to directory).
|
|
123
|
+
// Surface both so a path-key mismatch can't hide an active goal.
|
|
124
|
+
const worktrees = [api.state?.path?.worktree, api.state?.path?.directory];
|
|
110
125
|
|
|
111
126
|
api.slots.register({
|
|
112
127
|
order: 50,
|
|
113
128
|
slots: {
|
|
114
129
|
sidebar_content(_ctx, props) {
|
|
115
130
|
if (!props?.session_id) return undefined;
|
|
131
|
+
// The session's CURRENT agent, from its latest message (mirrors the
|
|
132
|
+
// reference OpenCode TUI plugin). This is the authoritative, immediate
|
|
133
|
+
// signal of whether the session is in Goal mode right now — so when the
|
|
134
|
+
// user switches to Build (or any non-goal agent) the Goal section vanishes
|
|
135
|
+
// and OpenCode's native todos return, without waiting for persisted state.
|
|
136
|
+
const currentAgent = () => {
|
|
137
|
+
try {
|
|
138
|
+
const msgs = api?.state?.session?.messages?.(props.session_id);
|
|
139
|
+
if (Array.isArray(msgs)) {
|
|
140
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
141
|
+
const a = msgs[i] && msgs[i].agent;
|
|
142
|
+
if (a) return String(a).toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
/* messages API unavailable — fall back to persisted goal state */
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
116
150
|
const read = () => {
|
|
117
151
|
try {
|
|
118
|
-
|
|
152
|
+
const agent = currentAgent();
|
|
153
|
+
// Render only in Goal mode. "goal" and its `goal-*` subagents count as
|
|
154
|
+
// Goal mode (so the section doesn't flicker out while reviewers run);
|
|
155
|
+
// any other primary agent (build/plan/custom) renders nothing here.
|
|
156
|
+
const goalMode = !agent || agent === GOAL_AGENT || agent.startsWith(`${GOAL_AGENT}-`);
|
|
157
|
+
if (!goalMode) return NO_GOAL;
|
|
158
|
+
return readModel(worktrees, props?.session_id) || NO_GOAL;
|
|
119
159
|
} catch {
|
|
120
160
|
return NO_GOAL;
|
|
121
161
|
}
|
|
122
162
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
// ALWAYS mount a reactive, polling component — do NOT bail when there is
|
|
164
|
+
// no goal yet. The goal is normally recorded AFTER the sidebar mounts
|
|
165
|
+
// (the user opens the session, then states the goal), so the slot must
|
|
166
|
+
// keep polling and let <Show> reveal the section when the goal appears.
|
|
167
|
+
// Returning undefined at mount (the old behavior) meant the poll never
|
|
168
|
+
// ran and the Goal section never showed even once a goal existed.
|
|
169
|
+
const first = read();
|
|
170
|
+
const [model, setModel] = createSignal(first);
|
|
127
171
|
const timer = setInterval(() => setModel(read()), POLL_MS);
|
|
128
|
-
const rainbowTimer = setTimeout(() => setRainbow(false), Math.max(0, rainbowMs || 0));
|
|
129
172
|
onCleanup(() => clearInterval(timer));
|
|
173
|
+
// First-display rainbow: starts the moment a goal FIRST appears. If a goal
|
|
174
|
+
// is already present at mount it starts immediately; otherwise the effect
|
|
175
|
+
// fires when the goal later appears (the common case — the goal is set
|
|
176
|
+
// after the sidebar mounts). Either way it settles after rainbowMs.
|
|
177
|
+
const [rainbow, setRainbow] = createSignal(false);
|
|
178
|
+
let rainbowStarted = false;
|
|
179
|
+
let rainbowTimer;
|
|
180
|
+
const startRainbow = () => {
|
|
181
|
+
if (rainbowStarted || (rainbowMs || 0) <= 0) return;
|
|
182
|
+
rainbowStarted = true;
|
|
183
|
+
setRainbow(true);
|
|
184
|
+
rainbowTimer = setTimeout(() => setRainbow(false), Math.max(0, rainbowMs));
|
|
185
|
+
};
|
|
186
|
+
if (first.state !== "none") startRainbow();
|
|
187
|
+
createEffect(() => {
|
|
188
|
+
if (model().state !== "none") startRainbow();
|
|
189
|
+
});
|
|
130
190
|
onCleanup(() => clearTimeout(rainbowTimer));
|
|
131
191
|
const isRainbow = () => rainbow() && model().state === "running";
|
|
132
192
|
// Settled (post-rainbow) colour for each header line. When done, every
|
|
@@ -136,7 +196,8 @@ const tui = async (api, options) => {
|
|
|
136
196
|
if (model().state === "done") return doneColor;
|
|
137
197
|
if (kind === "label") return color; // GOAL — yellow
|
|
138
198
|
if (kind === "title") return TITLE_COLOR; // goal title — bright white
|
|
139
|
-
return META_COLOR; //
|
|
199
|
+
if (kind === "gates") return META_COLOR; // gate count — cyan
|
|
200
|
+
return STATUS_COLOR; // lifecycle status — orange
|
|
140
201
|
};
|
|
141
202
|
const lineColor = (index, kind) => (isRainbow() ? RAINBOW[index % RAINBOW.length] : settled(kind));
|
|
142
203
|
const todoColor = (index, item) => {
|
|
@@ -144,17 +205,19 @@ const tui = async (api, options) => {
|
|
|
144
205
|
if (item.status === "done") return TODO_DONE_COLOR;
|
|
145
206
|
return model().state === "done" ? doneColor : muted;
|
|
146
207
|
};
|
|
147
|
-
// Goal sessions render a Goal-owned todo section
|
|
148
|
-
//
|
|
149
|
-
// no-goal sessions returned undefined
|
|
208
|
+
// Goal sessions render a Goal-owned todo section — GOAL label, goal title,
|
|
209
|
+
// gate count, lifecycle status, then structured todos — EACH on its own
|
|
210
|
+
// line in its own colour. Non-Goal / no-goal sessions returned undefined
|
|
211
|
+
// above, so the native todo section shows instead.
|
|
150
212
|
return (
|
|
151
213
|
<Show when={model().state !== "none"}>
|
|
152
214
|
<box flexDirection="column" paddingTop={1}>
|
|
153
215
|
<text fg={lineColor(0, "label")}><b>{model().label || "GOAL"}</b></text>
|
|
154
216
|
<text fg={lineColor(1, "title")}>{model().goal}</text>
|
|
155
|
-
<text fg={lineColor(2, "
|
|
217
|
+
<text fg={lineColor(2, "gates")}>{model().gates}</text>
|
|
218
|
+
<text fg={lineColor(3, "status")}>{model().status}</text>
|
|
156
219
|
<For each={model().todos || []}>
|
|
157
|
-
{(item, index) => <text fg={todoColor(index() +
|
|
220
|
+
{(item, index) => <text fg={todoColor(index() + 4, item)}>{`${item.status === "done" ? "✓" : "□"} ${item.text}`}</text>}
|
|
158
221
|
</For>
|
|
159
222
|
</box>
|
|
160
223
|
</Show>
|
package/scripts/install.mjs
CHANGED
|
@@ -183,6 +183,35 @@ function ensureTuiPlugin(remove = false) {
|
|
|
183
183
|
return true;
|
|
184
184
|
}
|
|
185
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
|
+
|
|
186
215
|
// ---------------------------------------------------------------------------
|
|
187
216
|
// Uninstall
|
|
188
217
|
// ---------------------------------------------------------------------------
|
|
@@ -205,6 +234,8 @@ if (values.uninstall) {
|
|
|
205
234
|
const tuiRemoved = ensureTuiPlugin(true);
|
|
206
235
|
if (!values["dry-run"]) pruneEmptyDirs(target, Object.keys(manifest.files));
|
|
207
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}).`);
|
|
208
239
|
const verb = values["dry-run"] ? "Would remove" : "Removed";
|
|
209
240
|
console.log(`${verb} ${removed.length} Goal Mode files from ${target}.`);
|
|
210
241
|
if (kept.length) {
|
|
@@ -302,4 +333,8 @@ console.log(
|
|
|
302
333
|
`Files copied: ${summary.copied.length}; unchanged: ${summary.unchanged.length}; pruned: ${summary.pruned.length}`,
|
|
303
334
|
);
|
|
304
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
|
+
}
|
|
305
340
|
console.log("Restart OpenCode for agents, commands, and plugins to load.");
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Auto-setup on a GLOBAL install.
|
|
4
|
+
*
|
|
5
|
+
* When a user runs `npm install -g opencode-goal-mode` (a fresh install OR an
|
|
6
|
+
* upgrade), this runs the installer for them so that, with a single command, the
|
|
7
|
+
* package's components are (re)copied into `~/.config/opencode`, the Goal sidebar
|
|
8
|
+
* is registered in `tui.json`, and OpenCode's stale plugin cache is cleared so the
|
|
9
|
+
* just-installed version actually loads. That makes `npm install -g` "really
|
|
10
|
+
* update" — no separate `opencode-goal-mode --global` step required.
|
|
11
|
+
*
|
|
12
|
+
* Safety:
|
|
13
|
+
* - Runs ONLY for global installs (npm sets `npm_config_global`). Repo development
|
|
14
|
+
* (`npm ci` / `npm install`) and installs as a project dependency are no-ops, so
|
|
15
|
+
* this never writes to a user's config when it shouldn't.
|
|
16
|
+
* - Best-effort: it NEVER fails the npm install. Any problem prints a one-line
|
|
17
|
+
* hint to run `opencode-goal-mode --global` manually and exits 0.
|
|
18
|
+
*/
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
|
|
23
|
+
function isGlobalInstall() {
|
|
24
|
+
const g = process.env.npm_config_global;
|
|
25
|
+
return g === "true" || g === "1" || g === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isGlobalInstall()) {
|
|
29
|
+
// Local/dev/dependency install — do not touch the user's OpenCode config.
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const installer = join(dirname(fileURLToPath(import.meta.url)), "install.mjs");
|
|
35
|
+
console.log("opencode-goal-mode: global install detected — updating ~/.config/opencode…");
|
|
36
|
+
const res = spawnSync(process.execPath, [installer, "--global"], { stdio: "inherit" });
|
|
37
|
+
// spawnSync does not throw on a non-zero exit (e.g. the installer refused to
|
|
38
|
+
// overwrite a file you edited). Surface a clear, actionable hint instead of
|
|
39
|
+
// leaving only the child's raw error.
|
|
40
|
+
if (res.error || res.status !== 0) {
|
|
41
|
+
console.warn(
|
|
42
|
+
"opencode-goal-mode: auto-setup didn't fully complete. " +
|
|
43
|
+
"Run `opencode-goal-mode --global` manually (add --force to replace files you've edited), then restart OpenCode.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`opencode-goal-mode: auto-setup skipped (${(err && err.message) || err}). ` +
|
|
49
|
+
"Run `opencode-goal-mode --global` to finish, then restart OpenCode.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
// Never fail `npm install`, regardless of the installer's outcome.
|
|
53
|
+
process.exit(0);
|