diff --git a/.zdp/plugin.json b/.zdp/plugin.json index fe3cf95..8ed7f37 100644 --- a/.zdp/plugin.json +++ b/.zdp/plugin.json @@ -2,16 +2,22 @@ "apiVersion": 1, "id": "scheduler", "name": "Scheduler", - "version": "0.1.4", + "version": "0.1.5", "description": "Create normal ZCode tasks from timezone-aware cron schedules while ZCode is open.", "entrypoints": { "main": "dist/main.cjs", "renderer": "dist/renderer.js" }, "engines": { - "host": ">=0.2.0 <1", + "host": ">=0.3.4 <1", "zcode": ">=3.3.6" }, + "capabilities": [ + "zcode.workspaces.read", + "zcode.tasks.run", + "ui.pages", + "ui.overlays" + ], "pages": [ {"id": "jobs", "title": "Scheduler"} ] diff --git a/README.md b/README.md index d8ca3a5..c14451b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Scheduled runs are ordinary persistent ZCode tasks. They appear immediately in t Open **Extensions → Available** in ZCode and install Scheduler. The host verifies the release archive checksum before staging it. New installs and updates take effect on the next ZCode launch. -Manual installation is also supported: download `zcode-scheduler-v0.1.4.zip` from the [latest release](https://github.com/notmike101/zcode-scheduler/releases/latest), extract the `scheduler` folder, and select it from **Extensions → Installed → Install folder**. +Manual installation is also supported: download `zcode-scheduler-v0.1.5.zip` from the [latest release](https://github.com/notmike101/zcode-scheduler/releases/latest), extract the `scheduler` folder, and select it from **Extensions → Installed → Install folder**. ## Scheduling behavior @@ -30,7 +30,7 @@ Manual installation is also supported: download `zcode-scheduler-v0.1.4.zip` fro ```powershell bun install bun run check -bun run release:package -- --tag v0.1.4 +bun run release:package -- --tag v0.1.5 ``` The release command writes a ZIP, its SHA-256 checksum, and `extension-update.json` to `dist/release`. diff --git a/bun.lock b/bun.lock index ea41574..36a50f8 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "zod": "^4.1.12", }, "devDependencies": { - "@notmike101/zcode-extension-sdk": "0.3.0", + "@notmike101/zcode-extension-sdk": "0.3.4", "@types/bun": "^1.3.4", "@types/node": "^24.10.1", "typescript": "^5.9.3", @@ -18,7 +18,7 @@ }, }, "packages": { - "@notmike101/zcode-extension-sdk": ["@notmike101/zcode-extension-sdk@0.3.0", "", {}, "sha512-MBZGF3+UPUQqHmaW8hUmqncG64oujaOawB5gMM0Vwl/4+rPr8pcoCs+fbzYhRXIKWIXZjsUnyE8ZmA6fkyINhQ=="], + "@notmike101/zcode-extension-sdk": ["@notmike101/zcode-extension-sdk@0.3.4", "", {}, "sha512-Kl/ldrGuAxg/O0SIZ8REG23qZhwJWxB6Be6QwG605mFDTCgloAQ2dMnpe7U+xlfGyhLhoYdTt6l2ab6p2WStIw=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], diff --git a/package.json b/package.json index edaffa9..2b3ac6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zcode-scheduler", - "version": "0.1.4", + "version": "0.1.5", "description": "Schedule normal, visible ZCode tasks with timezone-aware cron expressions.", "private": true, "license": "MIT", @@ -24,7 +24,7 @@ "zod": "^4.1.12" }, "devDependencies": { - "@notmike101/zcode-extension-sdk": "0.3.0", + "@notmike101/zcode-extension-sdk": "0.3.4", "@types/bun": "^1.3.4", "@types/node": "^24.10.1", "typescript": "^5.9.3" diff --git a/src/renderer.tsx b/src/renderer.tsx index 1b2d72b..30f3d73 100644 --- a/src/renderer.tsx +++ b/src/renderer.tsx @@ -1,6 +1,6 @@ import {render} from "preact"; import {useEffect, useMemo, useState} from "preact/hooks"; -import type {ExtensionBridge as ZdpBridge} from "@notmike101/zcode-extension-sdk/renderer"; +import {defineRendererExtension, type RendererExtensionContext} from "@notmike101/zcode-extension-sdk/renderer"; import type {RunRecord, SchedulerJob} from "./schemas.ts"; import styles from "./scheduler.css"; @@ -46,21 +46,27 @@ const emptyDraft = (timezone = Intl.DateTimeFormat().resolvedOptions().timeZone maxParallel: 4, }); -window.ZDP_REGISTER_PLUGIN_RENDERER?.({ +export const schedulerRenderer = defineRendererExtension({ id: "scheduler", - mount(container, bridge) { + mountPage(pageId, container, context) { + if (pageId !== "jobs") return; const style = document.createElement("style"); style.textContent = styles; container.replaceChildren(style); const mount = document.createElement("div"); mount.className = "scheduler-root"; container.append(mount); - render(, mount); - return () => render(null, mount); + render(, mount); + return () => { + render(null, mount); + container.replaceChildren(); + }; }, }); -function SchedulerPage({bridge}: {bridge: ZdpBridge}) { +if (typeof window !== "undefined") window.ZDP_REGISTER_PLUGIN_RENDERER?.(schedulerRenderer); + +function SchedulerPage({context}: {context: RendererExtensionContext}) { const [state, setState] = useState(); const [draft, setDraft] = useState(emptyDraft()); const [editing, setEditing] = useState(false); @@ -69,14 +75,13 @@ function SchedulerPage({bridge}: {bridge: ZdpBridge}) { const [error, setError] = useState(); const [busy, setBusy] = useState(false); - const invoke = (method: string, payload?: unknown) => bridge.invoke("plugin:invoke", {pluginId: "scheduler", method, payload}); + const invoke = (method: string, payload?: unknown) => context.ipc.invoke(method, payload); const refresh = () => invoke("get-state").then(setState).catch((cause) => setError(errorText(cause))); useEffect(() => { void refresh(); - const listener = () => void refresh(); - window.addEventListener("plugin:scheduler:state-changed", listener); - return () => window.removeEventListener("plugin:scheduler:state-changed", listener); + const subscription = context.ipc.on("state-changed", () => void refresh()); + return () => subscription.dispose(); }, []); useEffect(() => { @@ -95,7 +100,12 @@ function SchedulerPage({bridge}: {bridge: ZdpBridge}) { }; const save = async () => { - if (draft.mode === "yolo" && !window.confirm("Yolo mode permits unattended workspace changes. Save this job with Yolo permissions?")) return; + if (draft.mode === "yolo" && !await context.ui.showDialog({ + title: "Confirm Yolo permissions", + message: "Yolo mode permits unattended workspace changes. Save this job with Yolo permissions?", + confirmLabel: "Save job", + cancelLabel: "Cancel", + })) return; const model = draft.providerId.trim() && draft.modelId.trim() ? {providerId: draft.providerId.trim(), modelId: draft.modelId.trim(), ...(draft.variant.trim() ? {variant: draft.variant.trim()} : {})} : undefined; @@ -145,7 +155,7 @@ function SchedulerPage({bridge}: {bridge: ZdpBridge}) {
{tab === "jobs" && }
{error &&
{error}
} - {editing && tab === "jobs" && {setEditing(false); setDraft(emptyDraft(state?.timezone));}}/>} + {editing && tab === "jobs" && {setEditing(false); setDraft(emptyDraft(state?.timezone));}}/>} {!editing && tab === "jobs" &&
{state?.jobs.map((job) =>

{job.name}

{job.cron} · {job.timezone}

{job.enabled ? "Enabled" : "Paused"}
@@ -156,9 +166,15 @@ function SchedulerPage({bridge}: {bridge: ZdpBridge}) { {activeByJob.get(job.id) && } - +
)} {state && state.jobs.length === 0 &&
No scheduled jobs yet.
} @@ -167,9 +183,9 @@ function SchedulerPage({bridge}: {bridge: ZdpBridge}) { ; } -function JobEditor({draft, setDraft, preview, busy, bridge, save, cancel}: { +function JobEditor({draft, setDraft, preview, busy, context, save, cancel}: { draft: JobDraft; setDraft: (value: JobDraft) => void; preview?: {nextRuns: string[]; description: string}; busy: boolean; - bridge: ZdpBridge; save: () => Promise; cancel: () => void; + context: RendererExtensionContext; save: () => Promise; cancel: () => void; }) { const set = (key: K, value: JobDraft[K]) => setDraft({...draft, [key]: value}); return
@@ -177,7 +193,7 @@ function JobEditor({draft, setDraft, preview, busy, bridge, save, cancel}: { - +