@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.31",
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()