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
+22
View File
@@ -1574,6 +1574,8 @@ async def get_sessions(
min_messages: int = 0,
archived: str = "exclude",
order: str = "created",
source: str = None,
exclude_sources: str = None,
):
"""List sessions.
@@ -1604,7 +1606,14 @@ async def get_sessions(
min_message_count = max(0, min_messages)
archived_only = archived == "only"
include_archived = archived == "include"
# Optional source scoping: ``source`` includes a single class,
# ``exclude_sources`` (comma-separated) drops classes. The desktop
# uses these to split recents (exclude=cron) from the cron-jobs
# section (source=cron) into two independent lists.
exclude_list = [s for s in (exclude_sources or "").split(",") if s.strip()]
sessions = db.list_sessions_rich(
source=source or None,
exclude_sources=exclude_list or None,
limit=limit,
offset=offset,
min_message_count=min_message_count,
@@ -1613,6 +1622,8 @@ async def get_sessions(
order_by_last_active=order == "recent",
)
total = db.session_count(
source=source or None,
exclude_sources=exclude_list or None,
min_message_count=min_message_count,
include_archived=include_archived,
archived_only=archived_only,
@@ -1642,6 +1653,8 @@ async def get_profiles_sessions(
archived: str = "exclude",
order: str = "recent",
profile: str = "all",
source: str = None,
exclude_sources: str = None,
):
"""Unified, read-only session list aggregated across ALL profiles.
@@ -1677,6 +1690,11 @@ async def get_profiles_sessions(
min_message_count = max(0, min_messages)
archived_only = archived == "only"
include_archived = archived == "include"
# Source scoping (see /api/sessions): recents pass exclude_sources=cron,
# the cron-jobs section passes source=cron — two independent lists so
# newest cron sessions can't starve the recents page.
source_filter = source or None
exclude_list = [s for s in (exclude_sources or "").split(",") if s.strip()]
# Over-fetch per profile so the merged+sorted window is correct for the
# requested page. Capped so a huge profile can't blow up the response.
per_profile = min(max(limit + offset, limit), 500)
@@ -1700,6 +1718,8 @@ async def get_profiles_sessions(
continue
try:
rows = db.list_sessions_rich(
source=source_filter,
exclude_sources=exclude_list or None,
limit=per_profile,
offset=0,
min_message_count=min_message_count,
@@ -1708,6 +1728,8 @@ async def get_profiles_sessions(
order_by_last_active=order == "recent",
)
profile_total = db.session_count(
source=source_filter,
exclude_sources=exclude_list or None,
min_message_count=min_message_count,
include_archived=include_archived,
archived_only=archived_only,