@prevalentware/opencode-goal-plugin 0.1.31 → 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/package.json +1 -1
- package/src/tui.ts +69 -13
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()
|