@prevalentware/opencode-goal-plugin 0.1.17 → 0.1.19
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 +31 -7
- package/dist/server.js +897 -87
- package/package.json +5 -5
- package/src/tui.tsx +164 -45
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.19",
|
|
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",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"prepublishOnly": "bun run test && bun run build"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@opencode-ai/plugin": "^1.
|
|
56
|
+
"@opencode-ai/plugin": "^1.17.1",
|
|
57
57
|
"effect": "^3.21.2",
|
|
58
58
|
"zod": "^4.1.8"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@eslint/js": "^10.0.1",
|
|
62
|
-
"@opentui/core": "^0.
|
|
63
|
-
"@opentui/solid": "^0.
|
|
62
|
+
"@opentui/core": "^0.4.0",
|
|
63
|
+
"@opentui/solid": "^0.4.0",
|
|
64
64
|
"@types/bun": "^1.3.13",
|
|
65
65
|
"eslint": "^10.3.0",
|
|
66
66
|
"solid-js": "1.9.12",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"typescript-eslint": "^8.59.2"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
|
-
"opencode": ">=1.
|
|
71
|
+
"opencode": ">=1.17.1"
|
|
72
72
|
},
|
|
73
73
|
"publishConfig": {
|
|
74
74
|
"access": "public"
|
package/src/tui.tsx
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import type { TuiCommand, TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
3
|
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
|
4
4
|
|
|
5
|
+
type GoalCheckpoint = {
|
|
6
|
+
summary: string
|
|
7
|
+
timestamp: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type GoalHistoryEntry = {
|
|
11
|
+
type: string
|
|
12
|
+
detail: string
|
|
13
|
+
timestamp: number
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
type GoalSnapshot = {
|
|
6
17
|
sessionID: string
|
|
7
18
|
objective: string
|
|
8
|
-
status: "active" | "paused" | "budgetLimited" | "complete" | "unmet"
|
|
19
|
+
status: "active" | "paused" | "budgetLimited" | "usageLimited" | "complete" | "unmet"
|
|
9
20
|
tokenBudget: number | null
|
|
10
21
|
tokensUsed: number
|
|
11
22
|
timeUsedSeconds: number
|
|
@@ -16,6 +27,18 @@ type GoalSnapshot = {
|
|
|
16
27
|
closedAt?: number | null
|
|
17
28
|
continuationFailures: number
|
|
18
29
|
lastStatus: string | null
|
|
30
|
+
maxAutoTurns: number | null
|
|
31
|
+
maxDurationSeconds: number | null
|
|
32
|
+
noProgressTokenThreshold: number | null
|
|
33
|
+
maxNoProgressTurns: number | null
|
|
34
|
+
noProgressTurns: number
|
|
35
|
+
budgetWrapupSent: boolean
|
|
36
|
+
stopReason: string | null
|
|
37
|
+
history: GoalHistoryEntry[]
|
|
38
|
+
checkpoints: GoalCheckpoint[]
|
|
39
|
+
lastCheckpoint: GoalCheckpoint | null
|
|
40
|
+
lastAssistantText: string
|
|
41
|
+
lastAssistantMessageID: string
|
|
19
42
|
autoTurns: number
|
|
20
43
|
lastContinuationAt: number | null
|
|
21
44
|
remainingTokens: number | null
|
|
@@ -41,6 +64,22 @@ type GoalSessionState = {
|
|
|
41
64
|
messageIndex: number
|
|
42
65
|
}
|
|
43
66
|
|
|
67
|
+
type ModernTuiApi = TuiPluginApi & {
|
|
68
|
+
keymap?: {
|
|
69
|
+
registerLayer?: (layer: {
|
|
70
|
+
commands: {
|
|
71
|
+
namespace: string
|
|
72
|
+
name: string
|
|
73
|
+
title: string
|
|
74
|
+
desc?: string
|
|
75
|
+
category?: string
|
|
76
|
+
run?: () => void
|
|
77
|
+
}[]
|
|
78
|
+
bindings?: unknown[]
|
|
79
|
+
}) => () => void
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
const goalCache = new Map<string, GoalSnapshot>()
|
|
45
84
|
|
|
46
85
|
function currentSessionID(api: TuiPluginApi) {
|
|
@@ -69,31 +108,45 @@ function clearGoalPrompt() {
|
|
|
69
108
|
return "Clear the current session goal by calling clear_goal. Report whether a goal was cleared."
|
|
70
109
|
}
|
|
71
110
|
|
|
111
|
+
function pauseGoalPrompt() {
|
|
112
|
+
return 'Pause the current session goal by calling update_goal_status with status "paused". Report the result briefly.'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resumeGoalPrompt() {
|
|
116
|
+
return 'Resume the current session goal by calling update_goal_status with status "active", then continue working toward it.'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function historyGoalPrompt() {
|
|
120
|
+
return "Call get_goal_history for this session and report the current goal history briefly."
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function actionOption(api: TuiPluginApi, sessionID: string, title: string, value: string, description: string, prompt: string) {
|
|
124
|
+
return {
|
|
125
|
+
title,
|
|
126
|
+
value,
|
|
127
|
+
description,
|
|
128
|
+
onSelect: () => {
|
|
129
|
+
void sendGoalPrompt(api, sessionID, prompt)
|
|
130
|
+
.then(() => api.ui.dialog.clear())
|
|
131
|
+
.catch((error) => toast(api, error instanceof Error ? error.message : String(error), "error"))
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
72
136
|
function showSummary(api: TuiPluginApi, sessionID: string, goal: GoalSnapshot | null) {
|
|
73
137
|
const DialogSelect = api.ui.DialogSelect
|
|
74
138
|
const options = [
|
|
75
|
-
|
|
76
|
-
title: "Refresh",
|
|
77
|
-
value: "refresh",
|
|
78
|
-
description: "Ask the agent to read the current goal state",
|
|
79
|
-
onSelect: () => {
|
|
80
|
-
void sendGoalPrompt(api, sessionID, refreshGoalPrompt())
|
|
81
|
-
.then(() => api.ui.dialog.clear())
|
|
82
|
-
.catch((error) => toast(api, error instanceof Error ? error.message : String(error), "error"))
|
|
83
|
-
},
|
|
84
|
-
},
|
|
139
|
+
actionOption(api, sessionID, "Refresh", "refresh", "Ask the agent to read the current goal state", refreshGoalPrompt()),
|
|
85
140
|
...(goal
|
|
86
141
|
? [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
},
|
|
142
|
+
actionOption(api, sessionID, "History", "history", "Ask the agent to show lifecycle history", historyGoalPrompt()),
|
|
143
|
+
...(goal.status === "active"
|
|
144
|
+
? [actionOption(api, sessionID, "Pause", "pause", "Pause auto-continuation without clearing", pauseGoalPrompt())]
|
|
145
|
+
: []),
|
|
146
|
+
...(goal.status === "paused" || goal.status === "budgetLimited" || goal.status === "usageLimited"
|
|
147
|
+
? [actionOption(api, sessionID, "Resume", "resume", "Resume the goal and continue", resumeGoalPrompt())]
|
|
148
|
+
: []),
|
|
149
|
+
actionOption(api, sessionID, "Clear", "clear", "Ask the agent to clear this session goal", clearGoalPrompt()),
|
|
97
150
|
]
|
|
98
151
|
: []),
|
|
99
152
|
]
|
|
@@ -137,7 +190,7 @@ function currentEpochSeconds() {
|
|
|
137
190
|
|
|
138
191
|
export function liveTimeUsedSeconds(goal: GoalSnapshot, nowSeconds = currentEpochSeconds()) {
|
|
139
192
|
const baseSeconds = Math.max(0, Math.floor(goal.timeUsedSeconds))
|
|
140
|
-
if (
|
|
193
|
+
if (goal.status !== "active") return baseSeconds
|
|
141
194
|
if (typeof goal.sampledAt !== "number") return baseSeconds
|
|
142
195
|
return baseSeconds + Math.max(0, Math.floor(nowSeconds - goal.sampledAt))
|
|
143
196
|
}
|
|
@@ -146,11 +199,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
146
199
|
return typeof value === "object" && value !== null
|
|
147
200
|
}
|
|
148
201
|
|
|
202
|
+
function isCheckpoint(value: unknown): value is GoalCheckpoint {
|
|
203
|
+
return isRecord(value) && typeof value.summary === "string" && typeof value.timestamp === "number"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isHistoryEntry(value: unknown): value is GoalHistoryEntry {
|
|
207
|
+
return isRecord(value) && typeof value.type === "string" && typeof value.detail === "string" && typeof value.timestamp === "number"
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
150
211
|
if (!isRecord(value)) return false
|
|
151
212
|
if (typeof value.sessionID !== "string") return false
|
|
152
213
|
if (typeof value.objective !== "string") return false
|
|
153
|
-
if (!["active", "paused", "budgetLimited", "complete", "unmet"].includes(String(value.status))) return false
|
|
214
|
+
if (!["active", "paused", "budgetLimited", "usageLimited", "complete", "unmet"].includes(String(value.status))) return false
|
|
154
215
|
if (value.tokenBudget !== null && typeof value.tokenBudget !== "number") return false
|
|
155
216
|
if (typeof value.tokensUsed !== "number") return false
|
|
156
217
|
if (typeof value.timeUsedSeconds !== "number") return false
|
|
@@ -161,6 +222,18 @@ function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
|
161
222
|
if (value.closedAt != null && typeof value.closedAt !== "number") return false
|
|
162
223
|
if (typeof value.continuationFailures !== "number") return false
|
|
163
224
|
if (value.lastStatus != null && typeof value.lastStatus !== "string") return false
|
|
225
|
+
if (value.maxAutoTurns !== null && typeof value.maxAutoTurns !== "number") return false
|
|
226
|
+
if (value.maxDurationSeconds !== null && typeof value.maxDurationSeconds !== "number") return false
|
|
227
|
+
if (value.noProgressTokenThreshold !== null && typeof value.noProgressTokenThreshold !== "number") return false
|
|
228
|
+
if (value.maxNoProgressTurns !== null && typeof value.maxNoProgressTurns !== "number") return false
|
|
229
|
+
if (typeof value.noProgressTurns !== "number") return false
|
|
230
|
+
if (typeof value.budgetWrapupSent !== "boolean") return false
|
|
231
|
+
if (value.stopReason !== null && typeof value.stopReason !== "string") return false
|
|
232
|
+
if (!Array.isArray(value.history) || !value.history.every(isHistoryEntry)) return false
|
|
233
|
+
if (!Array.isArray(value.checkpoints) || !value.checkpoints.every(isCheckpoint)) return false
|
|
234
|
+
if (value.lastCheckpoint !== null && !isCheckpoint(value.lastCheckpoint)) return false
|
|
235
|
+
if (typeof value.lastAssistantText !== "string") return false
|
|
236
|
+
if (typeof value.lastAssistantMessageID !== "string") return false
|
|
164
237
|
if (typeof value.autoTurns !== "number") return false
|
|
165
238
|
if (value.lastContinuationAt != null && typeof value.lastContinuationAt !== "number") return false
|
|
166
239
|
if (value.remainingTokens !== null && typeof value.remainingTokens !== "number") return false
|
|
@@ -170,7 +243,19 @@ function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
|
170
243
|
|
|
171
244
|
function parseGoalToolOutput(part: GoalToolPart): GoalSnapshot | null | undefined {
|
|
172
245
|
if (part.type !== "tool") return undefined
|
|
173
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
![
|
|
248
|
+
"get_goal",
|
|
249
|
+
"get_goal_history",
|
|
250
|
+
"create_goal",
|
|
251
|
+
"set_goal",
|
|
252
|
+
"update_goal",
|
|
253
|
+
"update_goal_objective",
|
|
254
|
+
"update_goal_status",
|
|
255
|
+
"clear_goal",
|
|
256
|
+
].includes(part.tool ?? "")
|
|
257
|
+
)
|
|
258
|
+
return undefined
|
|
174
259
|
if (part.state?.status !== "completed") return undefined
|
|
175
260
|
if (part.tool === "clear_goal") return null
|
|
176
261
|
if (typeof part.state.output !== "string") return undefined
|
|
@@ -207,18 +292,20 @@ function goalFromSession(api: TuiPluginApi, sessionID: string) {
|
|
|
207
292
|
return goalStateFromSession(api, sessionID).goal
|
|
208
293
|
}
|
|
209
294
|
|
|
210
|
-
function visibleStatus(status: GoalSnapshot["status"]) {
|
|
211
|
-
return status === "budgetLimited" ? "active" : status
|
|
212
|
-
}
|
|
213
|
-
|
|
214
295
|
function formatGoal(goal: GoalSnapshot | null) {
|
|
215
296
|
if (!goal) return "No recent goal state found in this session."
|
|
216
297
|
const lines = [
|
|
217
298
|
`Objective: ${goal.objective}`,
|
|
218
|
-
`Status: ${
|
|
299
|
+
`Status: ${goal.status}`,
|
|
219
300
|
`Time used: ${formatDuration(goal.timeUsedSeconds)}`,
|
|
220
|
-
`
|
|
301
|
+
`Tokens: ${goal.tokensUsed}${goal.tokenBudget == null ? "" : `/${goal.tokenBudget}`}`,
|
|
302
|
+
`Auto-continues: ${goal.autoTurns}${goal.maxAutoTurns == null ? "" : `/${goal.maxAutoTurns}`}`,
|
|
221
303
|
]
|
|
304
|
+
if (goal.remainingTokens != null) lines.push(`Tokens remaining: ${goal.remainingTokens}`)
|
|
305
|
+
if (goal.maxDurationSeconds != null) lines.push(`Duration limit: ${formatDuration(goal.maxDurationSeconds)}`)
|
|
306
|
+
if (goal.noProgressTurns > 0) lines.push(`No-progress turns: ${goal.noProgressTurns}`)
|
|
307
|
+
if (goal.lastCheckpoint) lines.push(`Latest checkpoint: ${goal.lastCheckpoint.summary}`)
|
|
308
|
+
if (goal.stopReason) lines.push(`Stop reason: ${goal.stopReason}`)
|
|
222
309
|
if (goal.lastStatus) lines.push(`Last status: ${goal.lastStatus}`)
|
|
223
310
|
if (goal.completionEvidence) lines.push(`Completion evidence: ${goal.completionEvidence}`)
|
|
224
311
|
if (goal.blocker) lines.push(`Blocker: ${goal.blocker}`)
|
|
@@ -251,9 +338,22 @@ function GoalSidebar(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
251
338
|
<text fg={theme().text}>
|
|
252
339
|
<b>Goal</b>
|
|
253
340
|
</text>
|
|
254
|
-
<text fg={theme().textMuted}>Status: {
|
|
341
|
+
<text fg={theme().textMuted}>Status: {value().status}</text>
|
|
255
342
|
<text fg={theme().textMuted}>Time: {formatDuration(elapsed())}</text>
|
|
256
|
-
<text fg={theme().textMuted}>
|
|
343
|
+
<text fg={theme().textMuted}>
|
|
344
|
+
Tokens: {value().tokensUsed}
|
|
345
|
+
<Show when={value().tokenBudget}>{(budget: () => number) => <>/{budget()}</>}</Show>
|
|
346
|
+
</text>
|
|
347
|
+
<text fg={theme().textMuted}>
|
|
348
|
+
Auto-continues: {value().autoTurns}
|
|
349
|
+
<Show when={value().maxAutoTurns}>{(budget: () => number) => <>/{budget()}</>}</Show>
|
|
350
|
+
</text>
|
|
351
|
+
<Show when={value().lastCheckpoint}>
|
|
352
|
+
{(checkpoint: () => GoalCheckpoint) => <text fg={theme().textMuted}>Checkpoint: {checkpoint().summary}</text>}
|
|
353
|
+
</Show>
|
|
354
|
+
<Show when={value().stopReason}>
|
|
355
|
+
{(reason: () => string) => <text fg={theme().textMuted}>Stop: {reason()}</text>}
|
|
356
|
+
</Show>
|
|
257
357
|
<Show when={value().lastStatus}>
|
|
258
358
|
{(status: () => string) => <text fg={theme().textMuted}>{status()}</text>}
|
|
259
359
|
</Show>
|
|
@@ -271,6 +371,27 @@ function GoalSidebar(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
271
371
|
)
|
|
272
372
|
}
|
|
273
373
|
|
|
374
|
+
function registerGoalCommand(api: TuiPluginApi, command: TuiCommand) {
|
|
375
|
+
const modern = api as ModernTuiApi
|
|
376
|
+
if (modern.keymap?.registerLayer) {
|
|
377
|
+
modern.keymap.registerLayer({
|
|
378
|
+
commands: [
|
|
379
|
+
{
|
|
380
|
+
namespace: "palette",
|
|
381
|
+
name: command.value,
|
|
382
|
+
title: command.title,
|
|
383
|
+
desc: command.description,
|
|
384
|
+
category: command.category,
|
|
385
|
+
run: command.onSelect,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
bindings: [],
|
|
389
|
+
})
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
api.command?.register(() => [command])
|
|
393
|
+
}
|
|
394
|
+
|
|
274
395
|
const tui: TuiPlugin = async (api) => {
|
|
275
396
|
api.slots.register({
|
|
276
397
|
order: 125,
|
|
@@ -281,19 +402,17 @@ const tui: TuiPlugin = async (api) => {
|
|
|
281
402
|
},
|
|
282
403
|
})
|
|
283
404
|
|
|
284
|
-
api
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
showSummary(api, sessionID, goalFromSession(api, sessionID))
|
|
294
|
-
},
|
|
405
|
+
registerGoalCommand(api, {
|
|
406
|
+
title: "Goal",
|
|
407
|
+
value: "goal.show",
|
|
408
|
+
category: "Goal",
|
|
409
|
+
description: "View, pause, resume, or clear the long-running session goal",
|
|
410
|
+
onSelect: () => {
|
|
411
|
+
const sessionID = sessionIDOrToast(api)
|
|
412
|
+
if (!sessionID) return
|
|
413
|
+
showSummary(api, sessionID, goalFromSession(api, sessionID))
|
|
295
414
|
},
|
|
296
|
-
|
|
415
|
+
})
|
|
297
416
|
}
|
|
298
417
|
|
|
299
418
|
const plugin: TuiPluginModule = {
|