@prevalentware/opencode-goal-plugin 0.1.30 → 0.1.32
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/README.md +45 -14
- package/package.json +1 -1
- package/src/tui.ts +69 -13
package/README.md
CHANGED
|
@@ -38,7 +38,18 @@ Common use cases:
|
|
|
38
38
|
|
|
39
39
|
## Install
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Choose the instructions that match the CLI you run:
|
|
42
|
+
|
|
43
|
+
| OpenCode version | How to identify it | Instructions |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| OpenCode 1 stable | You run `opencode` and `opencode --version` prints `1.x` | [OpenCode 1](#opencode-1-stable) |
|
|
46
|
+
| OpenCode 2 beta | You run `opencode2` | [OpenCode 2](#opencode-2-beta) |
|
|
47
|
+
|
|
48
|
+
Do not mix the configuration formats. OpenCode 1 uses `plugin` and `tui.json`; OpenCode 2 uses `plugins` and the global `cli.json`.
|
|
49
|
+
|
|
50
|
+
### OpenCode 1 Stable
|
|
51
|
+
|
|
52
|
+
Install for the current project:
|
|
42
53
|
|
|
43
54
|
```bash
|
|
44
55
|
opencode plugin @prevalentware/opencode-goal-plugin
|
|
@@ -52,51 +63,53 @@ opencode plugin -g @prevalentware/opencode-goal-plugin
|
|
|
52
63
|
|
|
53
64
|
OpenCode detects both package entrypoints and writes the plugin into the server and TUI config targets.
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
The package also supports OpenCode 2 preview `0.0.0-next-17055`. Add it to the V2 server and TUI plugin lists:
|
|
66
|
+
For manual installation, add the package to both V1 config files.
|
|
58
67
|
|
|
59
68
|
`opencode.json`:
|
|
60
69
|
|
|
61
70
|
```json
|
|
62
71
|
{
|
|
63
|
-
"
|
|
72
|
+
"plugin": ["@prevalentware/opencode-goal-plugin"]
|
|
64
73
|
}
|
|
65
74
|
```
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
`tui.json`:
|
|
68
77
|
|
|
69
78
|
```json
|
|
70
79
|
{
|
|
71
|
-
"
|
|
80
|
+
"plugin": ["@prevalentware/opencode-goal-plugin"]
|
|
72
81
|
}
|
|
73
82
|
```
|
|
74
83
|
|
|
75
|
-
OpenCode 2
|
|
84
|
+
### OpenCode 2 Beta
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
Use this section only when running `opencode2`. Plugin release `0.1.30` and newer supports OpenCode 2 preview `0.0.0-next-17055` while remaining compatible with OpenCode 1.
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
Add the package to both V2 plugin lists:
|
|
80
89
|
|
|
81
90
|
`opencode.json`:
|
|
82
91
|
|
|
83
92
|
```json
|
|
84
93
|
{
|
|
85
|
-
"
|
|
94
|
+
"plugins": ["@prevalentware/opencode-goal-plugin"]
|
|
86
95
|
}
|
|
87
96
|
```
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
`~/.config/opencode/cli.json`:
|
|
90
99
|
|
|
91
100
|
```json
|
|
92
101
|
{
|
|
93
|
-
"
|
|
102
|
+
"plugins": ["@prevalentware/opencode-goal-plugin"]
|
|
94
103
|
}
|
|
95
104
|
```
|
|
96
105
|
|
|
106
|
+
OpenCode 2 does not read the V1 `tui.json` file. The server entrypoint comes from `opencode.json`, while the sidebar and palette integration come from `~/.config/opencode/cli.json`.
|
|
107
|
+
|
|
108
|
+
OpenCode 2 plugin APIs are still beta. This package pins its V2 development contract to the preview version above; later previews may require a compatible plugin update. V2 currently supports the goal command, tools, persistent state, usage accounting, idle continuation, Plan-mode safety, and TUI sidebar/palette integration. Goal-specific compaction context and recovery of already-running child sessions after a plugin restart remain V1-only because the current V2 plugin context does not expose equivalent hooks or history queries.
|
|
109
|
+
|
|
97
110
|
## Options
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
In OpenCode 1, server options use the package-and-options tuple in `opencode.json`:
|
|
100
113
|
|
|
101
114
|
```json
|
|
102
115
|
{
|
|
@@ -122,6 +135,24 @@ Server options can be configured in `opencode.json`:
|
|
|
122
135
|
}
|
|
123
136
|
```
|
|
124
137
|
|
|
138
|
+
In OpenCode 2, use the plugin object form instead:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"plugins": [
|
|
143
|
+
{
|
|
144
|
+
"package": "@prevalentware/opencode-goal-plugin",
|
|
145
|
+
"options": {
|
|
146
|
+
"auto_continue": true,
|
|
147
|
+
"max_auto_turns": 25,
|
|
148
|
+
"default_token_budget": 200000,
|
|
149
|
+
"restricted_agents": ["plan"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
125
156
|
Defaults:
|
|
126
157
|
|
|
127
158
|
- `auto_continue`: `true`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prevalentware/opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "OpenCode goal plugin that adds Codex-style long-running goal mode, /goal commands, persistence, and TUI status for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
package/src/tui.ts
CHANGED
|
@@ -111,6 +111,61 @@ function box(props: Record<string, unknown>, children: ElementChild[] = []) {
|
|
|
111
111
|
return element("box", props, children)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
type SlotRender = (props: { sessionID: string }) => unknown
|
|
115
|
+
type SlotDispose = () => void
|
|
116
|
+
|
|
117
|
+
const noopDispose: SlotDispose = () => {}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Registers a V2 TUI slot across both plugin-context generations.
|
|
121
|
+
*
|
|
122
|
+
* Early V2 previews exposed `ui.slot(name, render)`. Current previews expose a
|
|
123
|
+
* single options argument, `ui.slot({ append, render })`, and silently register
|
|
124
|
+
* nothing when handed the positional pair — which is how the goal sidebar and
|
|
125
|
+
* the palette keymap layer both disappeared. Branch on the callback arity so
|
|
126
|
+
* either host works, and tolerate hosts that return no disposer.
|
|
127
|
+
*/
|
|
128
|
+
export function registerSlotV2(context: TuiPluginV2.Context, name: string, render: SlotRender): SlotDispose {
|
|
129
|
+
const slot = context.ui.slot as unknown as (...args: unknown[]) => unknown
|
|
130
|
+
const dispose = slot.length <= 1 ? slot({ append: name, render }) : slot(name, render)
|
|
131
|
+
return typeof dispose === "function" ? (dispose as SlotDispose) : noopDispose
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reads a theme color by trying each candidate path in order, descending into a
|
|
136
|
+
* `default` leaf when the resolved node is a color group.
|
|
137
|
+
*
|
|
138
|
+
* Current previews expose a nested theme (`text.default`, `text.subdued`,
|
|
139
|
+
* `text.feedback.success`), while earlier previews and the V1 TUI expose flat
|
|
140
|
+
* keys (`text`, `textMuted`, `primary`). Passing a color *group* as `fg`
|
|
141
|
+
* renders nothing useful, so resolve to a leaf before handing it to OpenTUI.
|
|
142
|
+
*/
|
|
143
|
+
export function themeColorV2(theme: unknown, ...paths: readonly (readonly string[])[]): unknown {
|
|
144
|
+
for (const path of paths) {
|
|
145
|
+
let cursor: unknown = theme
|
|
146
|
+
for (const key of path) {
|
|
147
|
+
if (cursor === null || typeof cursor !== "object") {
|
|
148
|
+
cursor = undefined
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
cursor = (cursor as Record<string, unknown>)[key]
|
|
152
|
+
}
|
|
153
|
+
if (cursor !== null && typeof cursor === "object" && "default" in (cursor as Record<string, unknown>)) {
|
|
154
|
+
cursor = (cursor as Record<string, unknown>).default
|
|
155
|
+
}
|
|
156
|
+
if (cursor !== undefined && cursor !== null) return cursor
|
|
157
|
+
}
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function goalColorsV2(theme: unknown) {
|
|
162
|
+
return {
|
|
163
|
+
text: themeColorV2(theme, ["text", "default"], ["text"]),
|
|
164
|
+
muted: themeColorV2(theme, ["text", "subdued"], ["textMuted"]),
|
|
165
|
+
achieved: themeColorV2(theme, ["text", "feedback", "success"], ["primary"], ["text", "default"], ["text"]),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
114
169
|
function goalSnapshotKey(sessionID: string) {
|
|
115
170
|
return `goal-mode.snapshot.${sessionID}`
|
|
116
171
|
}
|
|
@@ -498,7 +553,7 @@ async function showSummaryV2(api: TuiPluginV2.Context, sessionID: string, goal:
|
|
|
498
553
|
}
|
|
499
554
|
|
|
500
555
|
function GoalSidebarV2(api: TuiPluginV2.Context, sessionID: string) {
|
|
501
|
-
const
|
|
556
|
+
const colors = goalColorsV2(api.theme)
|
|
502
557
|
const [cache, setCache] = api.storage.memory<{ goal: GoalSnapshot | null }>(`goal-mode.v2.${sessionID}`, {
|
|
503
558
|
initial: { goal: null },
|
|
504
559
|
})
|
|
@@ -523,26 +578,27 @@ function GoalSidebarV2(api: TuiPluginV2.Context, sessionID: string) {
|
|
|
523
578
|
if (!snapshot) return null
|
|
524
579
|
if (snapshot.status === "complete" || snapshot.status === "unmet") {
|
|
525
580
|
const elapsed = liveTimeUsedSeconds(snapshot)
|
|
526
|
-
return text({ fg: snapshot.status === "complete" ?
|
|
581
|
+
return text({ fg: snapshot.status === "complete" ? colors.achieved : colors.muted }, [
|
|
527
582
|
`${snapshot.status === "complete" ? "Goal achieved" : "Goal unmet"} (${formatDurationBadge(elapsed)})`,
|
|
528
583
|
])
|
|
529
584
|
}
|
|
530
585
|
return box({}, [
|
|
531
|
-
text({ fg:
|
|
532
|
-
text({ fg:
|
|
533
|
-
text({ fg:
|
|
534
|
-
text({ fg:
|
|
535
|
-
text({ fg:
|
|
536
|
-
...(snapshot.lastCheckpoint ? [text({ fg:
|
|
537
|
-
...(snapshot.stopReason ? [text({ fg:
|
|
538
|
-
...(snapshot.lastStatus ? [text({ fg:
|
|
539
|
-
text({ fg:
|
|
586
|
+
text({ fg: colors.text }, ["Goal"]),
|
|
587
|
+
text({ fg: colors.muted }, [`Status: ${snapshot.status}`]),
|
|
588
|
+
text({ fg: colors.muted }, [`Time: ${formatDuration(liveTimeUsedSeconds(snapshot, nowSeconds()))}`]),
|
|
589
|
+
text({ fg: colors.muted }, [`Tokens: ${snapshot.tokensUsed}${snapshot.tokenBudget == null ? "" : `/${snapshot.tokenBudget}`}`]),
|
|
590
|
+
text({ fg: colors.muted }, [`Auto-continues: ${snapshot.autoTurns}${snapshot.maxAutoTurns == null ? "" : `/${snapshot.maxAutoTurns}`}`]),
|
|
591
|
+
...(snapshot.lastCheckpoint ? [text({ fg: colors.muted }, [`Checkpoint: ${snapshot.lastCheckpoint.summary}`])] : []),
|
|
592
|
+
...(snapshot.stopReason ? [text({ fg: colors.muted }, [`Stop: ${snapshot.stopReason}`])] : []),
|
|
593
|
+
...(snapshot.lastStatus ? [text({ fg: colors.muted }, [snapshot.lastStatus])] : []),
|
|
594
|
+
text({ fg: colors.muted }, [snapshot.objective]),
|
|
540
595
|
])
|
|
541
596
|
}])
|
|
542
597
|
}
|
|
543
598
|
|
|
544
599
|
function GoalKeymapLayerV2(api: TuiPluginV2.Context) {
|
|
545
600
|
api.keymap.layer(() => ({
|
|
601
|
+
mode: "global",
|
|
546
602
|
commands: [
|
|
547
603
|
{
|
|
548
604
|
id: "goal.show",
|
|
@@ -573,8 +629,8 @@ function GoalKeymapLayerV2(api: TuiPluginV2.Context) {
|
|
|
573
629
|
* needs to dispose the two `ui.slot` registrations.
|
|
574
630
|
*/
|
|
575
631
|
export function setupTuiV2(context: TuiPluginV2.Context): TuiPluginV2.Cleanup {
|
|
576
|
-
const offSidebar = context
|
|
577
|
-
const offApp = context
|
|
632
|
+
const offSidebar = registerSlotV2(context, "sidebar.content", (props) => GoalSidebarV2(context, props.sessionID))
|
|
633
|
+
const offApp = registerSlotV2(context, "app", () => GoalKeymapLayerV2(context))
|
|
578
634
|
return () => {
|
|
579
635
|
offSidebar()
|
|
580
636
|
offApp()
|