Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-plugin-loop",
"version": "0.6.0",
"version": "0.6.1",
"description": "/loop command for opencode — run prompts on a schedule (fixed, adaptive, or maintenance), modeled after Claude Code's /loop",
"type": "module",
"main": "./dist/index.js",
Expand Down
9 changes: 9 additions & 0 deletions src/tui-feedback-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,12 @@ export function isLoopFeedbackToast(event: unknown): event is {
isVariant(event.properties.variant)
)
}

// Legacy fallback: servers older than 0.6.0 deliver task lists via toast
// instead of the feedback file, so the TUI still honors these events.
export function isLoopTaskListToast(event: unknown): event is {
type: "tui.toast.show"
properties: LoopFeedbackInput & Record<string, unknown>
} {
return isLoopFeedbackToast(event) && event.properties.variant === "info"
}
10 changes: 10 additions & 0 deletions src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import {
LOOP_COPY_TITLE,
createLoopFeedbackModel,
isLoopTaskListToast,
type LoopFeedbackInput,
} from "./tui-feedback-model.js"

Expand Down Expand Up @@ -207,8 +208,17 @@ export function createLoopTuiPlugin(
openFeedback({ message: payload.message, variant: "info" })
})

// Legacy fallback: servers older than 0.6.0 deliver task lists via
// toast, not the feedback file. Newer servers never send these toasts,
// so both channels can be active without double-opening.
const unsubscribe = api.event.on("tui.toast.show", (event) => {
if (!isLoopTaskListToast(event)) return
openFeedback(event.properties)
})

api.lifecycle.onDispose(() => {
unwatch()
unsubscribe()
close()
})
}
Expand Down
4 changes: 2 additions & 2 deletions tests/package-exports.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const builtDialogView = await readFile(
"utf8",
)

test("publishes the 0.6.0 release", () => {
assert.equal(packageJson.version, "0.6.0")
test("publishes the 0.6.1 release", () => {
assert.equal(packageJson.version, "0.6.1")
})

test("publishes explicit server and TUI plugin entrypoints", () => {
Expand Down
40 changes: 40 additions & 0 deletions tests/tui-plugin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function createFakeApi() {
const toasts = []
const logs = []
const layers = []
const eventHandlers = new Map()
let dialogEntry

const dialog = {
Expand Down Expand Up @@ -53,6 +54,12 @@ function createFakeApi() {
},
},
theme: { current: {} },
event: {
on(type, handler) {
eventHandlers.set(type, handler)
return () => eventHandlers.delete(type)
},
},
keymap: {
registerLayer(layer) {
const record = { layer, active: true }
Expand All @@ -74,6 +81,9 @@ function createFakeApi() {
__view() {
return dialogEntry?.view
},
__emit(type, event) {
eventHandlers.get(type)?.(event)
},
__toasts: toasts,
__logs: logs,
__layers: layers,
Expand Down Expand Up @@ -139,6 +149,36 @@ test("opens the dialog when task list feedback arrives", async () => {
assert.equal(api.__view().variant, "info")
})

test("opens the dialog for legacy toast feedback from older servers", async () => {
const api = createFakeApi()
const { plugin } = createTestLoopTuiPlugin()
await plugin(api)

api.__emit("tui.toast.show", {
type: "tui.toast.show",
properties: {
title: "Loop · opencode-plugin-loop",
message: "📋 1 loop task(s):\n [abc123] ▶ active • every 60s • work",
variant: "info",
duration: 5000,
},
})
assert.equal(api.ui.dialog.open, true)
assert.match(api.__view().message, /abc123/)

api.__view().onClose()
api.__emit("tui.toast.show", {
type: "tui.toast.show",
properties: {
title: "Loop · opencode-plugin-loop",
message: "🔁 Loop started [id=zzz999]",
variant: "success",
duration: 5000,
},
})
assert.equal(api.ui.dialog.open, false)
})

test("ignores stale feedback and feedback from other directories", async () => {
const api = createFakeApi()
const { plugin, emitLoop } = createTestLoopTuiPlugin()
Expand Down
Loading