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