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 e89f7f1cd..b9ede3fa4 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 bd3a05184..968174cb0 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 6d89cf95a..e4e5e9133 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