From 5ee0b6c23538df94e7a294126a13c7fb70de92ba Mon Sep 17 00:00:00 2001 From: Codeplane Agent Date: Mon, 1 Jun 2026 12:49:33 +0000 Subject: [PATCH] fix(cron): add stop button for automated tasks in web UI Add a Stop button to the shared session composer for active cron sessions, wired to a new `abort()` action that clears todos and calls the session abort endpoint. On the server, `SessionRoutes.abort` now cancels the linked `cronRunID` via `CronScheduler.cancelRun` before aborting the prompt. Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: codeplane-agent[bot] <287208015+codeplane-agent[bot]@users.noreply.github.com> --- .../composer/session-composer-region.tsx | 16 ++++++++++-- .../composer/session-composer-state.ts | 26 ++++++++++++++++++- .../src/server/routes/instance/session.ts | 13 ++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index e89f7f1cda..b9ede3fa43 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -2,6 +2,7 @@ import { Show, createEffect, createMemo, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { useNavigate } from "@solidjs/router" import { useSpring } from "@codeplane-ai/ui/motion-spring" +import { Button } from "@codeplane-ai/ui/button" import { Icon } from "@codeplane-ai/ui/icon" import { PromptInput } from "@/components/prompt-input" import { useLanguage } from "@/context/language" @@ -323,12 +324,23 @@ export function SessionComposerRegion(props: { >
- + {isCronSession() ? language.t("session.cron.readonly") : language.t("session.archived.readOnly")} + + +
diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index bd3a051848..968174cb07 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -1,4 +1,4 @@ -import { createEffect, createMemo, on, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import type { PermissionRequest, QuestionRequest, Todo } from "@codeplane-ai/sdk/v2" import { useParams } from "@solidjs/router" @@ -77,6 +77,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => dock: todos().length > 0 && live(), closing: false, opening: false, + aborting: false, }) const permissionResponding = createMemo(() => { @@ -102,6 +103,26 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => }) } + const abort = () => { + const id = params.id + if (!id || store.aborting) return + + batch(() => { + globalSync.todo.set(id, []) + sync.set("todo", id, []) + sync.set("session_status", id, idle) + setStore("aborting", true) + }) + + sdk.client.session + .abort({ sessionID: id }) + .catch((err: unknown) => { + const description = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description }) + }) + .finally(() => setStore("aborting", false)) + } + let timer: number | undefined let raf: number | undefined @@ -194,7 +215,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => permissionRequest, permissionResponding, decide, + abort, + aborting: () => store.aborting, todos, + working: busy, dock: () => store.dock, closing: () => store.closing, opening: () => store.opening, diff --git a/packages/codeplane/src/server/routes/instance/session.ts b/packages/codeplane/src/server/routes/instance/session.ts index 6d89cf95a0..e4e5e9133e 100644 --- a/packages/codeplane/src/server/routes/instance/session.ts +++ b/packages/codeplane/src/server/routes/instance/session.ts @@ -30,8 +30,11 @@ import { Bus } from "@/bus" import { NamedError } from "@codeplane-ai/shared/util/error" import { jsonRequest, runRequest } from "./trace" import { queryBoolean } from "@/server/query" +import { CronScheduler } from "@/cron" +import { makeRuntime } from "@/effect/run-service" const log = Log.create({ service: "server" }) +const cronSchedulerRuntime = makeRuntime(CronScheduler.Service, CronScheduler.defaultLayer) export const SessionRoutes = lazy(() => new Hono() @@ -439,6 +442,16 @@ export const SessionRoutes = lazy(() => async (c) => jsonRequest("SessionRoutes.abort", c, function* () { const sessionID = c.req.valid("param").sessionID + const session = yield* Session.Service + const info = yield* session.get(sessionID).pipe( + Effect.catch(() => Effect.succeed(undefined as Session.Info | undefined)), + ) + const cronRunID = info?.cronRunID + if (cronRunID) { + yield* Effect.tryPromise(() => cronSchedulerRuntime.runPromise((svc) => svc.cancelRun(cronRunID))).pipe( + Effect.catch(() => Effect.void), + ) + } const svc = yield* SessionPrompt.Service const aborted = new DOMException("Aborted", "AbortError") // Mark all pending+running queue rows cancelled BEFORE cancelling