diff --git a/apps/desktop/src/app/cron/index.tsx b/apps/desktop/src/app/cron/index.tsx index eb9e4257cf2e..e0c421e41057 100644 --- a/apps/desktop/src/app/cron/index.tsx +++ b/apps/desktop/src/app/cron/index.tsx @@ -43,6 +43,7 @@ import { requestModelOptions } from '@/lib/model-options' import { asText } from '@/lib/text' import { $cronFocusJobId, $cronJobs, setCronFocusJobId, setCronJobs, updateCronJobs } from '@/store/cron' import { notify, notifyError } from '@/store/notifications' +import { $profileScope, ALL_PROFILES } from '@/store/profile' import { useRefreshHotkey } from '../hooks/use-refresh-hotkey' import { @@ -293,15 +294,20 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt const [pendingDelete, setPendingDelete] = useState(null) const [deleting, setDeleting] = useState(false) + // Jobs live per-profile on disk and the list endpoint aggregates 'all' by + // default — scope the fetch to the sidebar's profile scope so this overlay + // and the sidebar (which share the $cronJobs atom) agree on what's shown. + const profileScope = useStore($profileScope) + const refresh = useCallback(async () => { try { - setCronJobs(await getCronJobs()) + setCronJobs(await getCronJobs(profileScope === ALL_PROFILES ? 'all' : profileScope)) } catch (err) { notifyError(err, c.failedLoad) } finally { setLoading(false) } - }, [c]) + }, [c, profileScope]) useRefreshHotkey(refresh) diff --git a/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx index 0c85d59d0b78..0147d45e4a8b 100644 --- a/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx @@ -204,4 +204,25 @@ describe('refreshSessions batches slices into one request', () => { }) ) }) + + it('scopes the cron-jobs fetch to the active profile (all → unified view)', async () => { + const { getCronJobs } = await import('@/hermes') + listSidebarSessions.mockResolvedValue(sidebar({ sessions: [], total: 0, profile_totals: {} })) + + const scoped = renderHook(() => useSessionListActions({ profileScope: 'work' })) + + await act(async () => { + await scoped.result.current.refreshCronJobs() + }) + + expect(getCronJobs).toHaveBeenLastCalledWith('work') + + const unified = renderHook(() => useSessionListActions({ profileScope: '__all__' })) + + await act(async () => { + await unified.result.current.refreshCronJobs() + }) + + expect(getCronJobs).toHaveBeenLastCalledWith('all') + }) }) diff --git a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts index 0b5e0e219161..38850cad3a77 100644 --- a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts @@ -123,16 +123,19 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg // Cron *jobs* drive the sidebar "Cron jobs" section. Jobs are created // synchronously (agent tool call or the cron UI), so refreshing here right // after an agent turn surfaces a new job immediately; the interval poll keeps - // next-run/state fresh as the scheduler advances them. + // next-run/state fresh as the scheduler advances them. Jobs live per-profile + // on disk and the list endpoint aggregates 'all' by default, so scope the + // fetch to the sidebar's profile scope — a concrete profile sees only its + // own jobs; ALL_PROFILES keeps the unified view. const refreshCronJobs = useCallback(async () => { try { - const jobs = await getCronJobs() + const jobs = await getCronJobs(profileScope === ALL_PROFILES ? 'all' : profileScope) setCronJobs(jobs) } catch { // Non-fatal: the cron section just keeps its last-known jobs. } - }, []) + }, [profileScope]) const refreshSessions = useCallback(async () => { const requestId = refreshSessionsRequestRef.current + 1 diff --git a/apps/desktop/src/hermes-cron-scope.test.ts b/apps/desktop/src/hermes-cron-scope.test.ts index a9d13256f751..ac94bc6698fd 100644 --- a/apps/desktop/src/hermes-cron-scope.test.ts +++ b/apps/desktop/src/hermes-cron-scope.test.ts @@ -56,4 +56,19 @@ describe('cron helpers are profile-scoped', () => { expect(call[0].profile).toBe('coder') } }) + + it('list accepts an explicit ?profile= for endpoint-level filtering', () => { + // profileScoped() routes the backend process; the list endpoint ALSO + // aggregates 'all' by default, so callers pass an explicit profile to + // filter what the endpoint returns (sidebar / cron overlay scoping). + void getCronJobs('worker_alpha') + expect(api.mock.calls.at(-1)?.[0].path).toBe('/api/cron/jobs?profile=worker_alpha') + + void getCronJobs('all') + expect(api.mock.calls.at(-1)?.[0].path).toBe('/api/cron/jobs?profile=all') + + // Omitting the arg keeps the legacy unfiltered path. + void getCronJobs() + expect(api.mock.calls.at(-1)?.[0].path).toBe('/api/cron/jobs') + }) }) diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index c91c015da5bf..637f9566b66c 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -956,10 +956,17 @@ export function testMessagingPlatform(platformId: string): Promise { +// Cron jobs are stored per-profile (/cron/jobs.json), and the +// backend's list endpoint defaults to 'all'. Pass a concrete profile key to +// list just that profile's jobs, or 'all' for the unified cross-profile view. +// Omitting the arg keeps the legacy 'all' default for non-profile callers. +// profileScoped() still rides along for backend-process routing. +export function getCronJobs(profile?: string): Promise { + const suffix = profile ? `?profile=${encodeURIComponent(profile)}` : '' + return window.hermesDesktop.api({ ...profileScoped(), - path: '/api/cron/jobs', + path: `/api/cron/jobs${suffix}`, timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } diff --git a/contributors/emails/gijs@digitalbase.eu b/contributors/emails/gijs@digitalbase.eu new file mode 100644 index 000000000000..ae8639955259 --- /dev/null +++ b/contributors/emails/gijs@digitalbase.eu @@ -0,0 +1 @@ +digitalbase