@prevalentware/opencode-goal-plugin 0.1.16 → 0.1.18
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 +32 -6
- package/dist/server.js +609 -67
- package/package.json +5 -5
- package/src/tui.tsx +176 -43
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.18",
|
|
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
|
|
@@ -14,6 +25,22 @@ type GoalSnapshot = {
|
|
|
14
25
|
completionEvidence?: string | null
|
|
15
26
|
blocker?: string | null
|
|
16
27
|
closedAt?: number | null
|
|
28
|
+
continuationFailures: number
|
|
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
|
|
42
|
+
autoTurns: number
|
|
43
|
+
lastContinuationAt: number | null
|
|
17
44
|
remainingTokens: number | null
|
|
18
45
|
sampledAt?: number
|
|
19
46
|
}
|
|
@@ -37,6 +64,22 @@ type GoalSessionState = {
|
|
|
37
64
|
messageIndex: number
|
|
38
65
|
}
|
|
39
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
|
+
|
|
40
83
|
const goalCache = new Map<string, GoalSnapshot>()
|
|
41
84
|
|
|
42
85
|
function currentSessionID(api: TuiPluginApi) {
|
|
@@ -65,31 +108,45 @@ function clearGoalPrompt() {
|
|
|
65
108
|
return "Clear the current session goal by calling clear_goal. Report whether a goal was cleared."
|
|
66
109
|
}
|
|
67
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
|
+
|
|
68
136
|
function showSummary(api: TuiPluginApi, sessionID: string, goal: GoalSnapshot | null) {
|
|
69
137
|
const DialogSelect = api.ui.DialogSelect
|
|
70
138
|
const options = [
|
|
71
|
-
|
|
72
|
-
title: "Refresh",
|
|
73
|
-
value: "refresh",
|
|
74
|
-
description: "Ask the agent to read the current goal state",
|
|
75
|
-
onSelect: () => {
|
|
76
|
-
void sendGoalPrompt(api, sessionID, refreshGoalPrompt())
|
|
77
|
-
.then(() => api.ui.dialog.clear())
|
|
78
|
-
.catch((error) => toast(api, error instanceof Error ? error.message : String(error), "error"))
|
|
79
|
-
},
|
|
80
|
-
},
|
|
139
|
+
actionOption(api, sessionID, "Refresh", "refresh", "Ask the agent to read the current goal state", refreshGoalPrompt()),
|
|
81
140
|
...(goal
|
|
82
141
|
? [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
},
|
|
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()),
|
|
93
150
|
]
|
|
94
151
|
: []),
|
|
95
152
|
]
|
|
@@ -133,7 +190,7 @@ function currentEpochSeconds() {
|
|
|
133
190
|
|
|
134
191
|
export function liveTimeUsedSeconds(goal: GoalSnapshot, nowSeconds = currentEpochSeconds()) {
|
|
135
192
|
const baseSeconds = Math.max(0, Math.floor(goal.timeUsedSeconds))
|
|
136
|
-
if (
|
|
193
|
+
if (goal.status !== "active") return baseSeconds
|
|
137
194
|
if (typeof goal.sampledAt !== "number") return baseSeconds
|
|
138
195
|
return baseSeconds + Math.max(0, Math.floor(nowSeconds - goal.sampledAt))
|
|
139
196
|
}
|
|
@@ -142,11 +199,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
142
199
|
return typeof value === "object" && value !== null
|
|
143
200
|
}
|
|
144
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
|
+
|
|
145
210
|
function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
146
211
|
if (!isRecord(value)) return false
|
|
147
212
|
if (typeof value.sessionID !== "string") return false
|
|
148
213
|
if (typeof value.objective !== "string") return false
|
|
149
|
-
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
|
|
150
215
|
if (value.tokenBudget !== null && typeof value.tokenBudget !== "number") return false
|
|
151
216
|
if (typeof value.tokensUsed !== "number") return false
|
|
152
217
|
if (typeof value.timeUsedSeconds !== "number") return false
|
|
@@ -155,6 +220,22 @@ function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
|
155
220
|
if (value.completionEvidence != null && typeof value.completionEvidence !== "string") return false
|
|
156
221
|
if (value.blocker != null && typeof value.blocker !== "string") return false
|
|
157
222
|
if (value.closedAt != null && typeof value.closedAt !== "number") return false
|
|
223
|
+
if (typeof value.continuationFailures !== "number") return false
|
|
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
|
|
237
|
+
if (typeof value.autoTurns !== "number") return false
|
|
238
|
+
if (value.lastContinuationAt != null && typeof value.lastContinuationAt !== "number") return false
|
|
158
239
|
if (value.remainingTokens !== null && typeof value.remainingTokens !== "number") return false
|
|
159
240
|
if (value.sampledAt != null && typeof value.sampledAt !== "number") return false
|
|
160
241
|
return true
|
|
@@ -162,7 +243,19 @@ function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
|
162
243
|
|
|
163
244
|
function parseGoalToolOutput(part: GoalToolPart): GoalSnapshot | null | undefined {
|
|
164
245
|
if (part.type !== "tool") return undefined
|
|
165
|
-
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
|
|
166
259
|
if (part.state?.status !== "completed") return undefined
|
|
167
260
|
if (part.tool === "clear_goal") return null
|
|
168
261
|
if (typeof part.state.output !== "string") return undefined
|
|
@@ -199,17 +292,21 @@ function goalFromSession(api: TuiPluginApi, sessionID: string) {
|
|
|
199
292
|
return goalStateFromSession(api, sessionID).goal
|
|
200
293
|
}
|
|
201
294
|
|
|
202
|
-
function visibleStatus(status: GoalSnapshot["status"]) {
|
|
203
|
-
return status === "budgetLimited" ? "active" : status
|
|
204
|
-
}
|
|
205
|
-
|
|
206
295
|
function formatGoal(goal: GoalSnapshot | null) {
|
|
207
296
|
if (!goal) return "No recent goal state found in this session."
|
|
208
297
|
const lines = [
|
|
209
298
|
`Objective: ${goal.objective}`,
|
|
210
|
-
`Status: ${
|
|
299
|
+
`Status: ${goal.status}`,
|
|
211
300
|
`Time used: ${formatDuration(goal.timeUsedSeconds)}`,
|
|
301
|
+
`Tokens: ${goal.tokensUsed}${goal.tokenBudget == null ? "" : `/${goal.tokenBudget}`}`,
|
|
302
|
+
`Auto-continues: ${goal.autoTurns}${goal.maxAutoTurns == null ? "" : `/${goal.maxAutoTurns}`}`,
|
|
212
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}`)
|
|
309
|
+
if (goal.lastStatus) lines.push(`Last status: ${goal.lastStatus}`)
|
|
213
310
|
if (goal.completionEvidence) lines.push(`Completion evidence: ${goal.completionEvidence}`)
|
|
214
311
|
if (goal.blocker) lines.push(`Blocker: ${goal.blocker}`)
|
|
215
312
|
return lines.join("\n")
|
|
@@ -241,8 +338,25 @@ function GoalSidebar(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
241
338
|
<text fg={theme().text}>
|
|
242
339
|
<b>Goal</b>
|
|
243
340
|
</text>
|
|
244
|
-
<text fg={theme().textMuted}>Status: {
|
|
341
|
+
<text fg={theme().textMuted}>Status: {value().status}</text>
|
|
245
342
|
<text fg={theme().textMuted}>Time: {formatDuration(elapsed())}</text>
|
|
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>
|
|
357
|
+
<Show when={value().lastStatus}>
|
|
358
|
+
{(status: () => string) => <text fg={theme().textMuted}>{status()}</text>}
|
|
359
|
+
</Show>
|
|
246
360
|
<text fg={theme().textMuted}>{objective()}</text>
|
|
247
361
|
</box>
|
|
248
362
|
}
|
|
@@ -257,6 +371,27 @@ function GoalSidebar(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
257
371
|
)
|
|
258
372
|
}
|
|
259
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
|
+
|
|
260
395
|
const tui: TuiPlugin = async (api) => {
|
|
261
396
|
api.slots.register({
|
|
262
397
|
order: 125,
|
|
@@ -267,19 +402,17 @@ const tui: TuiPlugin = async (api) => {
|
|
|
267
402
|
},
|
|
268
403
|
})
|
|
269
404
|
|
|
270
|
-
api
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
showSummary(api, sessionID, goalFromSession(api, sessionID))
|
|
280
|
-
},
|
|
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))
|
|
281
414
|
},
|
|
282
|
-
|
|
415
|
+
})
|
|
283
416
|
}
|
|
284
417
|
|
|
285
418
|
const plugin: TuiPluginModule = {
|