mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
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:
parent
e3ae035921
commit
628f9040df
12 changed files with 151 additions and 9 deletions
|
|
@ -44,6 +44,7 @@ import {
|
|||
$panesFlipped,
|
||||
$pinnedSessionIds,
|
||||
$sidebarAgentsGrouped,
|
||||
$sidebarCronOpen,
|
||||
$sidebarOpen,
|
||||
$sidebarPinsOpen,
|
||||
$sidebarRecentsOpen,
|
||||
|
|
@ -51,6 +52,7 @@ import {
|
|||
reorderPinnedSession,
|
||||
SESSION_SEARCH_FOCUS_EVENT,
|
||||
setSidebarAgentsGrouped,
|
||||
setSidebarCronOpen,
|
||||
setSidebarPinsOpen,
|
||||
setSidebarRecentsOpen,
|
||||
SIDEBAR_SESSIONS_PAGE_SIZE,
|
||||
|
|
@ -65,6 +67,7 @@ import {
|
|||
normalizeProfileKey
|
||||
} from '@/store/profile'
|
||||
import {
|
||||
$cronSessions,
|
||||
$selectedStoredSessionId,
|
||||
$sessionProfileTotals,
|
||||
$sessions,
|
||||
|
|
@ -243,8 +246,10 @@ export function ChatSidebar({
|
|||
const pinnedSessionIds = useStore($pinnedSessionIds)
|
||||
const pinsOpen = useStore($sidebarPinsOpen)
|
||||
const agentsOpen = useStore($sidebarRecentsOpen)
|
||||
const cronOpen = useStore($sidebarCronOpen)
|
||||
const selectedSessionId = useStore($selectedStoredSessionId)
|
||||
const sessions = useStore($sessions)
|
||||
const cronSessions = useStore($cronSessions)
|
||||
const sessionsLoading = useStore($sessionsLoading)
|
||||
const sessionsTotal = useStore($sessionsTotal)
|
||||
const sessionProfileTotals = useStore($sessionProfileTotals)
|
||||
|
|
@ -323,7 +328,10 @@ export function ChatSidebar({
|
|||
const sessionByAnyId = useMemo(() => {
|
||||
const map = new Map<string, SessionInfo>()
|
||||
|
||||
for (const s of visibleSessions) {
|
||||
// Cron sessions are listed separately but can still be pinned, so index
|
||||
// them too — otherwise a pinned cron job can't resolve into the Pinned
|
||||
// section. Recents take precedence on id collisions (set last).
|
||||
for (const s of [...cronSessions, ...visibleSessions]) {
|
||||
map.set(s.id, s)
|
||||
|
||||
if (s._lineage_root_id && !map.has(s._lineage_root_id)) {
|
||||
|
|
@ -332,7 +340,7 @@ export function ChatSidebar({
|
|||
}
|
||||
|
||||
return map
|
||||
}, [visibleSessions])
|
||||
}, [visibleSessions, cronSessions])
|
||||
|
||||
const pinnedSessions = useMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
|
|
@ -405,6 +413,17 @@ export function ChatSidebar({
|
|||
return [...out.values()]
|
||||
}, [trimmedQuery, sortedSessions, serverMatches, sessionByAnyId])
|
||||
|
||||
// Cron-job sessions are a fully independent list (fetched separately so they
|
||||
// never consume the recents page budget). Scope them like recents and drop
|
||||
// any that are pinned (pin wins) to avoid a double-listing.
|
||||
const visibleCronSessions = useMemo(() => {
|
||||
const scoped = showAllProfiles
|
||||
? cronSessions
|
||||
: cronSessions.filter(s => normalizeProfileKey(s.profile) === profileScope)
|
||||
|
||||
return scoped.filter(s => !pinnedRealIdSet.has(s.id)).sort((a, b) => sessionTime(b) - sessionTime(a))
|
||||
}, [cronSessions, showAllProfiles, profileScope, pinnedRealIdSet])
|
||||
|
||||
const unpinnedAgentSessions = useMemo(
|
||||
() => sortedSessions.filter(s => !pinnedRealIdSet.has(s.id)),
|
||||
[sortedSessions, pinnedRealIdSet]
|
||||
|
|
@ -482,7 +501,10 @@ export function ChatSidebar({
|
|||
])
|
||||
|
||||
const showSessionSkeletons = sessionsLoading && sortedSessions.length === 0
|
||||
const showSessionSections = showSessionSkeletons || sortedSessions.length > 0
|
||||
|
||||
const showSessionSections =
|
||||
showSessionSkeletons || sortedSessions.length > 0 || visibleCronSessions.length > 0
|
||||
|
||||
// Pagination is scope-aware. In "All profiles" mode it tracks the global
|
||||
// unified set. When scoped to one profile it must compare that profile's own
|
||||
// loaded rows against that profile's total — otherwise a huge default profile
|
||||
|
|
@ -759,6 +781,26 @@ export function ChatSidebar({
|
|||
/>
|
||||
)}
|
||||
|
||||
{sidebarOpen && showSessionSections && !trimmedQuery && visibleCronSessions.length > 0 && (
|
||||
<SidebarSessionsSection
|
||||
activeSessionId={activeSidebarSessionId}
|
||||
contentClassName="flex max-h-64 shrink-0 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75"
|
||||
emptyState={null}
|
||||
label={s.cronJobs}
|
||||
labelMeta={String(visibleCronSessions.length)}
|
||||
onArchiveSession={onArchiveSession}
|
||||
onDeleteSession={onDeleteSession}
|
||||
onResumeSession={onResumeSession}
|
||||
onToggle={() => setSidebarCronOpen(!cronOpen)}
|
||||
onTogglePin={pinSession}
|
||||
open={cronOpen}
|
||||
pinned={false}
|
||||
rootClassName="shrink-0 p-0 pb-1"
|
||||
sessions={visibleCronSessions}
|
||||
workingSessionIdSet={workingSessionIdSet}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarOpen && !showSessionSections && <div className="min-h-0 flex-1" />}
|
||||
|
||||
{sidebarOpen && (
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
sessionPinId,
|
||||
setAwaitingResponse,
|
||||
setBusy,
|
||||
setCronSessions,
|
||||
setCurrentBranch,
|
||||
setCurrentCwd,
|
||||
setCurrentModel,
|
||||
|
|
@ -101,6 +102,11 @@ const ProfilesView = lazy(async () => ({ default: (await import('./profiles')).P
|
|||
const SettingsView = lazy(async () => ({ default: (await import('./settings')).SettingsView }))
|
||||
const SkillsView = lazy(async () => ({ default: (await import('./skills')).SkillsView }))
|
||||
|
||||
// Latest cron-job sessions surfaced in the collapsed "Cron jobs" section. The
|
||||
// section shows the most-recent jobs, not the full history (that lives in
|
||||
// search), so this stays small and is fetched as a single bounded page.
|
||||
const CRON_SECTION_LIMIT = 50
|
||||
|
||||
// Rows a session refresh must preserve even if the aggregator omits them:
|
||||
// in-flight first turns (message_count 0), pinned rows aged off the page, and
|
||||
// the actively-viewed chat (its "working" flag clears a beat before the
|
||||
|
|
@ -224,6 +230,21 @@ export function DesktopController() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Cron-job sessions as their own list (latest N). Independent of the recents
|
||||
// page so the two never compete for slots. Cheap + bounded; refreshed
|
||||
// alongside recents.
|
||||
const refreshCronSessions = useCallback(async () => {
|
||||
try {
|
||||
const { sessions } = await listAllProfileSessions(CRON_SECTION_LIMIT, 1, 'exclude', 'recent', 'all', {
|
||||
source: 'cron'
|
||||
})
|
||||
|
||||
setCronSessions(sessions)
|
||||
} catch {
|
||||
// Non-fatal: the cron section just stays empty/stale.
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshSessions = useCallback(async () => {
|
||||
const requestId = refreshSessionsRequestRef.current + 1
|
||||
refreshSessionsRequestRef.current = requestId
|
||||
|
|
@ -231,13 +252,18 @@ export function DesktopController() {
|
|||
|
||||
try {
|
||||
const limit = $sessionsLimit.get()
|
||||
|
||||
// Require at least one message so abandoned/empty "Untitled" drafts (one
|
||||
// was created per TUI/desktop launch before the lazy-create fix) don't
|
||||
// clutter the sidebar.
|
||||
// Unified cross-profile list (served read-only off each profile's
|
||||
// state.db; no per-profile backend is spawned). Single-profile users get
|
||||
// the same rows tagged profile="default".
|
||||
const result = await listAllProfileSessions(limit, 1)
|
||||
// the same rows tagged profile="default". Cron sessions are excluded here
|
||||
// and fetched separately (refreshCronSessions) so the scheduler's
|
||||
// always-newest rows can't consume the recents page budget.
|
||||
const result = await listAllProfileSessions(limit, 1, 'exclude', 'recent', 'all', {
|
||||
excludeSources: ['cron']
|
||||
})
|
||||
|
||||
if (refreshSessionsRequestRef.current === requestId) {
|
||||
setSessions(prev => mergeSessionPage(prev, result.sessions, sessionsToKeep()))
|
||||
|
|
@ -249,7 +275,9 @@ export function DesktopController() {
|
|||
setSessionsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
void refreshCronSessions()
|
||||
}, [refreshCronSessions])
|
||||
|
||||
const loadMoreSessions = useCallback(() => {
|
||||
bumpSessionsLimit()
|
||||
|
|
@ -262,7 +290,11 @@ export function DesktopController() {
|
|||
const key = normalizeProfileKey(profile)
|
||||
const inKey = (s: SessionInfo) => normalizeProfileKey(s.profile) === key
|
||||
const loaded = $sessions.get().filter(inKey).length
|
||||
const result = await listAllProfileSessions(loaded + SIDEBAR_SESSIONS_PAGE_SIZE, 1, 'exclude', 'recent', key)
|
||||
|
||||
const result = await listAllProfileSessions(loaded + SIDEBAR_SESSIONS_PAGE_SIZE, 1, 'exclude', 'recent', key, {
|
||||
excludeSources: ['cron']
|
||||
})
|
||||
|
||||
const keep = sessionsToKeep(key)
|
||||
|
||||
setSessions(prev => [...prev.filter(s => !inKey(s)), ...mergeSessionPage(prev.filter(inKey), result.sessions, keep)])
|
||||
|
|
|
|||
|
|
@ -149,17 +149,33 @@ export async function listSessions(
|
|||
// primary backend straight off each profile's state.db — no per-profile backend
|
||||
// is spawned. Single-profile users get the same rows as listSessions(), tagged
|
||||
// profile="default".
|
||||
// Source scoping lets callers split the unified list into independent slices:
|
||||
// recents pass `excludeSources: ['cron']`, the cron-jobs section passes
|
||||
// `source: 'cron'`. Without this a burst of (always-newest) cron sessions
|
||||
// consumes the whole recents page and starves real conversations.
|
||||
export interface SessionSourceFilter {
|
||||
source?: string
|
||||
excludeSources?: string[]
|
||||
}
|
||||
|
||||
export async function listAllProfileSessions(
|
||||
limit = 40,
|
||||
minMessages = 0,
|
||||
archived: 'exclude' | 'include' | 'only' = 'exclude',
|
||||
order: 'created' | 'recent' = 'recent',
|
||||
profile: 'all' | (string & {}) = 'all'
|
||||
profile: 'all' | (string & {}) = 'all',
|
||||
filter: SessionSourceFilter = {}
|
||||
): Promise<PaginatedSessions> {
|
||||
const sourceParam = filter.source ? `&source=${encodeURIComponent(filter.source)}` : ''
|
||||
|
||||
const excludeParam = filter.excludeSources?.length
|
||||
? `&exclude_sources=${encodeURIComponent(filter.excludeSources.join(','))}`
|
||||
: ''
|
||||
|
||||
const result = await window.hermesDesktop.api<PaginatedSessions>({
|
||||
path:
|
||||
`/api/profiles/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}` +
|
||||
`&archived=${archived}&order=${order}&profile=${encodeURIComponent(profile)}`
|
||||
`&archived=${archived}&order=${order}&profile=${encodeURIComponent(profile)}${sourceParam}${excludeParam}`
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,7 @@ export const en: Translations = {
|
|||
results: 'Results',
|
||||
pinned: 'Pinned',
|
||||
sessions: 'Sessions',
|
||||
cronJobs: 'Cron jobs',
|
||||
groupAriaGrouped: 'Show sessions as a single list',
|
||||
groupAriaUngrouped: 'Group sessions by workspace',
|
||||
groupTitleGrouped: 'Ungroup sessions',
|
||||
|
|
|
|||
|
|
@ -1147,6 +1147,7 @@ export const ja = defineLocale({
|
|||
results: '結果',
|
||||
pinned: 'ピン留め',
|
||||
sessions: 'セッション',
|
||||
cronJobs: 'Cronジョブ',
|
||||
groupAriaGrouped: 'セッションを単一リストとして表示',
|
||||
groupAriaUngrouped: 'ワークスペースごとにセッションをグループ化',
|
||||
groupTitleGrouped: 'セッションのグループ化を解除',
|
||||
|
|
|
|||
|
|
@ -801,6 +801,7 @@ export interface Translations {
|
|||
results: string
|
||||
pinned: string
|
||||
sessions: string
|
||||
cronJobs: string
|
||||
groupAriaGrouped: string
|
||||
groupAriaUngrouped: string
|
||||
groupTitleGrouped: string
|
||||
|
|
|
|||
|
|
@ -1113,6 +1113,7 @@ export const zhHant = defineLocale({
|
|||
results: '結果',
|
||||
pinned: '已釘選',
|
||||
sessions: '工作階段',
|
||||
cronJobs: '排程任務',
|
||||
groupAriaGrouped: '以單一清單顯示工作階段',
|
||||
groupAriaUngrouped: '依工作區分組工作階段',
|
||||
groupTitleGrouped: '取消分組',
|
||||
|
|
|
|||
|
|
@ -1191,6 +1191,7 @@ export const zh: Translations = {
|
|||
results: '结果',
|
||||
pinned: '已置顶',
|
||||
sessions: '会话',
|
||||
cronJobs: '定时任务',
|
||||
groupAriaGrouped: '以单一列表显示会话',
|
||||
groupAriaUngrouped: '按工作区分组会话',
|
||||
groupTitleGrouped: '取消分组',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3198,6 +3198,7 @@ class SessionDB:
|
|||
include_archived: bool = False,
|
||||
archived_only: bool = False,
|
||||
exclude_children: bool = False,
|
||||
exclude_sources: List[str] = None,
|
||||
) -> int:
|
||||
"""Count sessions, optionally filtered by source.
|
||||
|
||||
|
|
@ -3207,6 +3208,11 @@ class SessionDB:
|
|||
is paired with a ``list_sessions_rich`` page (e.g. sidebar "load more"
|
||||
totals) so the total matches the number of listable rows — otherwise the
|
||||
raw row count is inflated by children and "load more" never settles.
|
||||
|
||||
Pass ``exclude_sources`` to drop whole source classes from the count
|
||||
(e.g. ``["cron"]`` so the recents "load more" total matches a
|
||||
cron-excluded ``list_sessions_rich`` page and doesn't keep "load more"
|
||||
stuck on for buried scheduler sessions).
|
||||
"""
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
|
@ -3225,6 +3231,10 @@ class SessionDB:
|
|||
if source:
|
||||
where_clauses.append("s.source = ?")
|
||||
params.append(source)
|
||||
if exclude_sources:
|
||||
placeholders = ",".join("?" for _ in exclude_sources)
|
||||
where_clauses.append(f"s.source NOT IN ({placeholders})")
|
||||
params.extend(exclude_sources)
|
||||
if min_message_count > 0:
|
||||
where_clauses.append("s.message_count >= ?")
|
||||
params.append(min_message_count)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue