fix: active-only session filter + paginated "Show N more" (closes #901) (#903)

* test: reproduce issue #901 (active-only filter + paginated show more)

* fix: active-only session filter + paginated 'Show N more' (closes #901)

Add a persisted 'Active sessions only' toggle in Settings -> Display that
hides inactive sessions in the sidebar while keeping the selected session
visible. Change 'Show N more' to reveal one batch (preview-limit size) per
click instead of expanding every hidden session at once, with 'Show less'
to collapse back to the initial preview.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
SSU-WEI HUANG
2026-06-18 10:16:42 +08:00
committed by GitHub
co-authored by HAPI
parent 22bf7e04d2
commit f5c0ef245b
6 changed files with 230 additions and 21 deletions
+70 -21
View File
@@ -13,6 +13,7 @@ import { useTranslation } from '@/lib/use-translation'
import { DEFAULT_SESSION_PREVIEW_LIMIT, useSessionPreviewLimit } from '@/hooks/useSessionPreviewLimit'
import { AgentFlavorIcon } from '@/components/AgentFlavorIcon'
import { useSessionListStatusMode } from '@/hooks/useSessionListStatusMode'
import { useShowActiveSessionsOnly } from '@/hooks/useShowActiveSessionsOnly'
import { classifySessionAttention } from '@/lib/sessionAttention'
import { getSessionLastSeenAt } from '@/lib/sessionLastSeen'
import { getAttentionLabel, SessionAttentionIndicator } from '@/components/SessionAttentionIndicator'
@@ -173,6 +174,20 @@ export function prepareSidebarSessions(sessions: SessionSummary[], selectedSessi
.filter(session => shouldShowSessionInSidebar(session, selectedSessionId))
}
// "Active sessions only" view: hide inactive sessions, but never hide the one the
// operator currently has open — otherwise toggling the filter would yank the
// selected session out from under them.
export function filterActiveSessionsOnly(sessions: SessionSummary[], selectedSessionId?: string | null): SessionSummary[] {
return sessions.filter(session => session.active || session.id === selectedSessionId)
}
// Paginated "Show N more": reveal one batch (step) at a time instead of expanding
// every hidden session at once. Always advances by at least one and never exceeds
// the total so the button reliably reaches a fully-expanded state.
export function getNextSessionVisibleCount(current: number, step: number, total: number): number {
return Math.min(current + Math.max(1, step), total)
}
function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] {
const groups = new Map<string, { directory: string; machineId: string | null; sessions: SessionSummary[] }>()
@@ -793,6 +808,7 @@ export function SessionList(props: {
const { renderHeader = true, api, selectedSessionId, machineLabelsById = {}, onNewSessionInDirectory } = props
const { sessionPreviewLimit } = useSessionPreviewLimit()
const { sessionListStatusMode } = useSessionListStatusMode()
const { showActiveSessionsOnly } = useShowActiveSessionsOnly()
const showDetailedStatus = sessionListStatusMode === 'detailed'
const [searchQuery, setSearchQuery] = useState('')
const [, setCodexImportedSessionsVersion] = useState(0)
@@ -817,8 +833,11 @@ export function SessionList(props: {
}
const allSessions = useMemo(
() => prepareSidebarSessions(props.sessions, selectedSessionId),
[props.sessions, selectedSessionId]
() => {
const prepared = prepareSidebarSessions(props.sessions, selectedSessionId)
return showActiveSessionsOnly ? filterActiveSessionsOnly(prepared, selectedSessionId) : prepared
},
[props.sessions, selectedSessionId, showActiveSessionsOnly]
)
const visibleSessions = useMemo(
() => isSearching
@@ -860,20 +879,30 @@ export function SessionList(props: {
})
}
const isSessionGroupExpanded = (group: SessionGroup): boolean => {
if (isSearching || group.sessions.length <= sessionPreviewLimit) return true
const key = `sessions::${group.key}`
const override = collapseOverrides.get(key)
if (override !== undefined) return !override
return false
// Per-group reveal cap for paginated "Show N more". Absent = collapsed to the
// preview limit; each "Show more" bumps it by one batch (step = preview limit).
const [sessionVisibleCounts, setSessionVisibleCounts] = useState<Map<string, number>>(
() => new Map()
)
const getGroupVisibleCount = (group: SessionGroup): number => {
return sessionVisibleCounts.get(group.key) ?? sessionPreviewLimit
}
const toggleSessionGroup = (group: SessionGroup) => {
const key = `sessions::${group.key}`
const expanded = isSessionGroupExpanded(group)
setCollapseOverrides(prev => {
const showMoreSessions = (group: SessionGroup) => {
setSessionVisibleCounts(prev => {
const next = new Map(prev)
next.set(key, expanded)
const current = prev.get(group.key) ?? sessionPreviewLimit
next.set(group.key, getNextSessionVisibleCount(current, sessionPreviewLimit, group.sessions.length))
return next
})
}
const collapseSessionGroup = (group: SessionGroup) => {
setSessionVisibleCounts(prev => {
if (!prev.has(group.key)) return prev
const next = new Map(prev)
next.delete(group.key)
return next
})
}
@@ -882,9 +911,9 @@ export function SessionList(props: {
return getVisibleSessionPreview(
group.sessions,
{
expanded: isSessionGroupExpanded(group),
expanded: isSearching,
selectedSessionId,
limit: sessionPreviewLimit
limit: getGroupVisibleCount(group)
}
)
}
@@ -959,6 +988,23 @@ export function SessionList(props: {
})
}, [allGroups])
// Clean up reveal caps for groups that no longer exist.
useEffect(() => {
setSessionVisibleCounts(prev => {
if (prev.size === 0) return prev
const knownKeys = new Set(allGroups.map(g => g.key))
const next = new Map(prev)
let changed = false
for (const key of next.keys()) {
if (!knownKeys.has(key)) {
next.delete(key)
changed = true
}
}
return changed ? next : prev
})
}, [allGroups])
return (
<div className="mx-auto w-full max-w-content flex flex-col">
{renderHeader ? (
@@ -1021,7 +1067,8 @@ export function SessionList(props: {
const isCollapsed = isGroupCollapsed(group)
const visibleGroupSessions = getVisibleGroupSessions(group)
const hiddenSessionCount = group.sessions.length - visibleGroupSessions.length
const sessionGroupExpanded = isSessionGroupExpanded(group)
const canCollapseSessions = getGroupVisibleCount(group) > sessionPreviewLimit
const showMoreCount = Math.min(sessionPreviewLimit, hiddenSessionCount)
const canStartInGroupDirectory = group.directory !== 'Other'
return (
<div key={group.key}>
@@ -1072,18 +1119,20 @@ export function SessionList(props: {
showDetailedStatus={showDetailedStatus}
/>
))}
{!isSearching && group.sessions.length > sessionPreviewLimit && (sessionGroupExpanded || hiddenSessionCount > 0) ? (
{!isSearching && group.sessions.length > sessionPreviewLimit && (hiddenSessionCount > 0 || canCollapseSessions) ? (
<button
type="button"
onClick={() => toggleSessionGroup(group)}
onClick={() => hiddenSessionCount > 0
? showMoreSessions(group)
: collapseSessionGroup(group)}
className={cn(
'mx-2 my-1 rounded-md px-2 py-1 text-left text-xs text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]',
hiddenSessionCount > 0 && 'border border-dashed border-[var(--app-border)]'
)}
>
{sessionGroupExpanded
? t('sessions.group.showLess')
: t('sessions.group.showMore', { n: hiddenSessionCount })}
{hiddenSessionCount > 0
? t('sessions.group.showMore', { n: showMoreCount })
: t('sessions.group.showLess')}
</button>
) : null}
</div>