diff --git a/package-lock.json b/package-lock.json index cd4065d..2b9fb5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-plugin-loop", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-plugin-loop", - "version": "0.6.0", + "version": "0.6.1", "license": "MIT", "dependencies": { "clipboardy": "4.0.0" diff --git a/package.json b/package.json index 232a1ec..3b47720 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/tui-feedback-model.ts b/src/tui-feedback-model.ts index c2009d8..e06252f 100644 --- a/src/tui-feedback-model.ts +++ b/src/tui-feedback-model.ts @@ -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 +} { + return isLoopFeedbackToast(event) && event.properties.variant === "info" +} diff --git a/src/tui.ts b/src/tui.ts index 9142425..579dfad 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -27,6 +27,7 @@ import { import { LOOP_COPY_TITLE, createLoopFeedbackModel, + isLoopTaskListToast, type LoopFeedbackInput, } from "./tui-feedback-model.js" @@ -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() }) } diff --git a/tests/package-exports.test.mjs b/tests/package-exports.test.mjs index 0d809b8..d060726 100644 --- a/tests/package-exports.test.mjs +++ b/tests/package-exports.test.mjs @@ -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", () => { diff --git a/tests/tui-plugin.test.mjs b/tests/tui-plugin.test.mjs index 0c5ef22..081d46d 100644 --- a/tests/tui-plugin.test.mjs +++ b/tests/tui-plugin.test.mjs @@ -11,6 +11,7 @@ function createFakeApi() { const toasts = [] const logs = [] const layers = [] + const eventHandlers = new Map() let dialogEntry const dialog = { @@ -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 } @@ -74,6 +81,9 @@ function createFakeApi() { __view() { return dialogEntry?.view }, + __emit(type, event) { + eventHandlers.get(type)?.(event) + }, __toasts: toasts, __logs: logs, __layers: layers, @@ -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()