feat(desktop): split cron sessions into their own sidebar section

Scheduler sessions (source=cron) were listed in recents, where their
`[IMPORTANT: …]` first-message previews spammed the list — and because
cron runs are always newest, a burst of them consumed the whole recents
page budget and starved real conversations (sidebar showed 0 sessions).

Recents and cron jobs are now two independent lists:
- Backend: /api/sessions + /api/profiles/sessions accept source /
  exclude_sources; session_count gains exclude_sources. Recents query
  excludes cron; the cron section queries source=cron.
- Desktop: separate $cronSessions store + refreshCronSessions fetch, a
  collapsed (persisted) "Cron jobs" section below Sessions that only
  renders when cron sessions exist, with its own bounded scroller.
This commit is contained in:
Brooklyn Nicholson
2026-06-06 12:30:39 -05:00
parent e3ae035921
commit 628f9040df
12 changed files with 151 additions and 9 deletions
+10
View File
@@ -22,6 +22,7 @@ export const SIDEBAR_SESSIONS_PAGE_SIZE = 50
const SIDEBAR_PINNED_STORAGE_KEY = 'hermes.desktop.pinnedSessions'
const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'hermes.desktop.agentsGroupedByWorkspace'
const SIDEBAR_CRON_OPEN_STORAGE_KEY = 'hermes.desktop.sidebarCronOpen'
const PANES_FLIPPED_STORAGE_KEY = 'hermes.desktop.panesFlipped'
export const CHAT_SIDEBAR_PANE_ID = 'chat-sidebar'
@@ -54,6 +55,10 @@ export const $sidebarWidth: ReadableAtom<number> = computed($paneStates, states
export const $pinnedSessionIds = atom(storedStringArray(SIDEBAR_PINNED_STORAGE_KEY))
export const $sidebarPinsOpen = atom(true)
export const $sidebarRecentsOpen = atom(true)
// Cron-job sessions live in their own section below recents, collapsed by
// default (it only renders at all when cron sessions exist) so the
// scheduler's `[IMPORTANT: …]` first-message previews don't spam recents.
export const $sidebarCronOpen = atom(storedBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, false))
export const $sidebarAgentsGrouped = atom(storedBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, false))
// When true, the sessions sidebar moves to the right and the file browser +
// preview rail move to the left — a mirror of the default layout.
@@ -62,6 +67,7 @@ export const $isSidebarResizing = atom(false)
export const $sessionsLimit = atom(SIDEBAR_SESSIONS_PAGE_SIZE)
$pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY, [...ids]))
$sidebarCronOpen.subscribe(open => persistBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, open))
$sidebarAgentsGrouped.subscribe(grouped => persistBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, grouped))
$panesFlipped.subscribe(flipped => persistBoolean(PANES_FLIPPED_STORAGE_KEY, flipped))
@@ -114,6 +120,10 @@ export function setSidebarRecentsOpen(open: boolean) {
$sidebarRecentsOpen.set(open)
}
export function setSidebarCronOpen(open: boolean) {
$sidebarCronOpen.set(open)
}
export function setSidebarAgentsGrouped(grouped: boolean) {
$sidebarAgentsGrouped.set(grouped)
}
+5
View File
@@ -76,6 +76,10 @@ export const $connection = atom<HermesConnection | null>(null)
export const $gatewayState = atom('idle')
export const $sessions = atom<SessionInfo[]>([])
export const $sessionsTotal = atom<number>(0)
// Cron-job sessions (source === 'cron') are fetched as their own list so the
// scheduler's always-newest sessions never crowd recents out of the page
// budget. Powers the collapsed "Cron jobs" sidebar section.
export const $cronSessions = atom<SessionInfo[]>([])
// Listable conversation count per profile (children excluded), keyed by profile
// name. Lets the sidebar scope its "Load more" footer to the active profile so a
// huge default profile doesn't keep "Load more" visible while browsing a small
@@ -119,6 +123,7 @@ export const setConnection = (next: Updater<HermesConnection | null>) => updateA
export const setGatewayState = (next: Updater<string>) => updateAtom($gatewayState, next)
export const setSessions = (next: Updater<SessionInfo[]>) => updateAtom($sessions, next)
export const setSessionsTotal = (next: Updater<number>) => updateAtom($sessionsTotal, next)
export const setCronSessions = (next: Updater<SessionInfo[]>) => updateAtom($cronSessions, next)
export const setSessionProfileTotals = (next: Updater<Record<string, number>>) =>
updateAtom($sessionProfileTotals, next)
export const setSessionsLoading = (next: Updater<boolean>) => updateAtom($sessionsLoading, next)