@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.
Files changed (3) hide show
  1. package/README.md +45 -14
  2. package/package.json +1 -1
  3. 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
- Install locally for the current OpenCode project:
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
- ### OpenCode 2 Beta
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
- "plugins": ["@prevalentware/opencode-goal-plugin"]
72
+ "plugin": ["@prevalentware/opencode-goal-plugin"]
64
73
  }
65
74
  ```
66
75
 
67
- `~/.config/opencode/cli.json`:
76
+ `tui.json`:
68
77
 
69
78
  ```json
70
79
  {
71
- "plugins": ["@prevalentware/opencode-goal-plugin"]
80
+ "plugin": ["@prevalentware/opencode-goal-plugin"]
72
81
  }
73
82
  ```
74
83
 
75
- 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.
84
+ ### OpenCode 2 Beta
76
85
 
77
- ## Manual Config
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
- If you configure it manually, add the package to both config files.
88
+ Add the package to both V2 plugin lists:
80
89
 
81
90
  `opencode.json`:
82
91
 
83
92
  ```json
84
93
  {
85
- "plugin": ["@prevalentware/opencode-goal-plugin"]
94
+ "plugins": ["@prevalentware/opencode-goal-plugin"]
86
95
  }
87
96
  ```
88
97
 
89
- `tui.json`:
98
+ `~/.config/opencode/cli.json`:
90
99
 
91
100
  ```json
92
101
  {
93
- "plugin": ["@prevalentware/opencode-goal-plugin"]
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
- Server options can be configured in `opencode.json`:
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.30",
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 theme = api.theme
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" ? theme.primary : theme.textMuted }, [
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: theme.text }, ["Goal"]),
532
- text({ fg: theme.textMuted }, [`Status: ${snapshot.status}`]),
533
- text({ fg: theme.textMuted }, [`Time: ${formatDuration(liveTimeUsedSeconds(snapshot, nowSeconds()))}`]),
534
- text({ fg: theme.textMuted }, [`Tokens: ${snapshot.tokensUsed}${snapshot.tokenBudget == null ? "" : `/${snapshot.tokenBudget}`}`]),
535
- text({ fg: theme.textMuted }, [`Auto-continues: ${snapshot.autoTurns}${snapshot.maxAutoTurns == null ? "" : `/${snapshot.maxAutoTurns}`}`]),
536
- ...(snapshot.lastCheckpoint ? [text({ fg: theme.textMuted }, [`Checkpoint: ${snapshot.lastCheckpoint.summary}`])] : []),
537
- ...(snapshot.stopReason ? [text({ fg: theme.textMuted }, [`Stop: ${snapshot.stopReason}`])] : []),
538
- ...(snapshot.lastStatus ? [text({ fg: theme.textMuted }, [snapshot.lastStatus])] : []),
539
- text({ fg: theme.textMuted }, [snapshot.objective]),
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.ui.slot("sidebar.content", (props) => GoalSidebarV2(context, props.sessionID))
577
- const offApp = context.ui.slot("app", () => GoalKeymapLayerV2(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()