diff --git a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx
index c8487140b89..b4b0feaf95a 100644
--- a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx
+++ b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx
@@ -1,15 +1,18 @@
import { useStore } from '@nanostores/react'
import { useEffect, useMemo, useState } from 'react'
+import { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
import { Codicon } from '@/components/ui/codicon'
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
import { SidebarGroup, SidebarGroupContent } from '@/components/ui/sidebar'
import { Tip } from '@/components/ui/tooltip'
-import { getCronJobRuns, type SessionInfo } from '@/hermes'
+import { deleteCronJob, getCronJobRuns, pauseCronJob, resumeCronJob, type SessionInfo } from '@/hermes'
import { useI18n } from '@/i18n'
import { fmtDayTime, relativeTime } from '@/lib/time'
import { cn } from '@/lib/utils'
+import { updateCronJobs } from '@/store/cron'
+import { notify, notifyError } from '@/store/notifications'
import { $selectedStoredSessionId } from '@/store/session'
import type { CronJob } from '@/types/hermes'
@@ -191,75 +194,128 @@ function CronJobSidebarRow({
const state = jobState(job)
const next = nextRunMs(job)
const label = jobTitle(job)
+ const isPaused = state === 'paused'
const meta = INACTIVE_STATES.has(state) ? (c.states[state] ?? state) : next !== null ? relativeTime(next, nowMs) : '—'
+ // Pause/resume and delete aren't threaded through the sidebar's prop chain, so
+ // drive them against the shared $cronJobs atom directly (same path the cron
+ // overlay uses) — the sidebar and overlay render from that one atom, so the
+ // row updates in place.
+ const togglePause = async () => {
+ try {
+ const updated = isPaused ? await resumeCronJob(job.id) : await pauseCronJob(job.id)
+ updateCronJobs(rows => rows.map(row => (row.id === job.id ? updated : row)))
+ notify({ kind: 'success', title: isPaused ? c.resumed : c.paused, message: label })
+ } catch (err) {
+ notifyError(err, c.failedUpdate)
+ }
+ }
+
+ const remove = async () => {
+ if (!window.confirm(`${c.deleteDescPrefix}${label}${c.deleteDescSuffix}`)) {
+ return
+ }
+
+ try {
+ await deleteCronJob(job.id)
+ updateCronJobs(rows => rows.filter(row => row.id !== job.id))
+ notify({ kind: 'success', title: c.deleted, message: label })
+ } catch (err) {
+ notifyError(err, c.failedDelete)
+ }
+ }
+
+ // One action set for both the hover buttons and the right-click menu.
+ const items = (kit: MenuKit) => (
+ <>
+ {renderActionItem(kit, { icon: 'zap', key: 'trigger', label: c.triggerNow, onSelect: onTrigger })}
+ {renderActionItem(kit, {
+ icon: isPaused ? 'play' : 'debug-pause',
+ key: 'pause',
+ label: isPaused ? c.resume : c.pause,
+ onSelect: () => void togglePause()
+ })}
+ {renderActionItem(kit, { icon: 'watch', key: 'manage', label: c.manage, onSelect: onManage })}
+
+ {renderActionItem(kit, {
+ icon: 'trash',
+ key: 'delete',
+ label: t.common.delete,
+ onSelect: () => void remove(),
+ variant: 'destructive'
+ })}
+ >
+ )
+
return (
-
- {/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use
- so the cron dots line up with the sessions above; the caret sits next
- to the label (matching the other sidebar disclosures) and the whole
- label area toggles the run peek. */}
-
-