fix: include active root sessions in Recent sidebar
Recent now shows active root sessions immediately, even if they are older than 48 hours Archived sessions and subtasks still stay out of Recent Added coverage and documentation for the new recency rules
This commit is contained in:
@@ -78,6 +78,7 @@ import {
|
||||
orderSessionsByLifecycleScopes,
|
||||
useSessionOrderingStore,
|
||||
} from '@/sync/session-ordering';
|
||||
import { useGlobalSessionStatusStore } from '@/sync/global-session-status';
|
||||
import {
|
||||
refreshGlobalSessions,
|
||||
refreshGlobalSessionsForDirectories,
|
||||
@@ -270,6 +271,10 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
(state) => isVisible ? state.rankById : EMPTY_SESSION_ORDER_RANKS,
|
||||
[isVisible],
|
||||
));
|
||||
const activeSessionIds = useGlobalSessionStatusStore(useShallow(
|
||||
(state) => isVisible ? [...state.statusById.keys()].sort() : EMPTY_STRING_ARRAY,
|
||||
));
|
||||
const activeSessionIdSet = React.useMemo(() => new Set(activeSessionIds), [activeSessionIds]);
|
||||
const togglePinnedSession = useSessionPinnedStore((state) => state.toggle);
|
||||
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(() => {
|
||||
try {
|
||||
@@ -1263,16 +1268,16 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
return meta;
|
||||
}, [projectSections, homeDirectory]);
|
||||
|
||||
const activeNowSessions = React.useMemo(() => {
|
||||
const recentSessions = React.useMemo(() => {
|
||||
if (!showRecentSection || isVSCode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return deriveRecentSessions(sessions)
|
||||
return deriveRecentSessions(sessions, activeSessionIdSet)
|
||||
.sort((a, b) => compareSessionsByLifecycleOrder(a, b, pinnedSessionIds, sessionOrderRanks));
|
||||
}, [isVSCode, pinnedSessionIds, sessionOrderRanks, sessions, showRecentSection]);
|
||||
}, [activeSessionIdSet, isVSCode, pinnedSessionIds, sessionOrderRanks, sessions, showRecentSection]);
|
||||
|
||||
// Prefetch is wired below, after recentSessionIds is computed.
|
||||
// Prefetch is wired below, after recentSessions is computed.
|
||||
|
||||
const activitySections = React.useMemo(() => {
|
||||
// VS Code renders the full grouped project view (one group per open
|
||||
@@ -1282,8 +1287,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const recentSessions = activeNowSessions;
|
||||
|
||||
const toItem = (session: Session) => {
|
||||
const existing = sessionSidebarMetaById.get(session.id);
|
||||
const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
|
||||
@@ -1316,7 +1319,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
return [
|
||||
{ key: 'active-now' as const, title: t('sessions.sidebar.activity.recentTitle'), items },
|
||||
];
|
||||
}, [activeNowSessions, filterSessionNodesForSearch, hasSessionSearchQuery, isVSCode, normalizedSessionSearchQuery, sessionSidebarMetaById, showRecentSection, t]);
|
||||
}, [filterSessionNodesForSearch, hasSessionSearchQuery, isVSCode, normalizedSessionSearchQuery, recentSessions, sessionSidebarMetaById, showRecentSection, t]);
|
||||
|
||||
const hasActivitySectionItems = React.useMemo(
|
||||
() => activitySections.some((section) => section.items.length > 0),
|
||||
@@ -1728,7 +1731,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
<SessionPrefetchEffect
|
||||
enabled={isVisible}
|
||||
sortedSessions={orderedSessions}
|
||||
recentSessions={activeNowSessions}
|
||||
recentSessions={recentSessions}
|
||||
prefetchSession={sync.prefetchSession}
|
||||
/>
|
||||
<SidebarHeader
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
- Folder membership may contain both a parent session and its descendants. Rendering treats only the highest assigned ancestors as folder roots because their normal session trees already include assigned descendants; persisted membership remains unchanged for cleanup and move semantics.
|
||||
- Sidebar selection holds the clicked row's viewport position across navigation-driven sidebar updates. Wheel or touch input cancels the hold immediately, so programmatic compensation never fights intentional scrolling.
|
||||
- Global session subscriptions are structural: create/delete, title, share, archive, directory, parent, and slug changes invalidate the tree. Recency-only `time.updated` changes do not trigger a rebuild. The separate lifecycle rank invalidates ordering only on `settled ↔ active` transitions, with root sessions ranked among roots and child sessions only among siblings of the same parent.
|
||||
- Recent membership includes active root sessions immediately even when their last committed `time.updated` falls outside the 48-hour window. Children and archived sessions remain excluded, and inactive roots remain timestamp-based. The active-ID subscription is disabled while the sidebar is hidden and ignores retry/status detail changes, avoiding streaming-frequency rerenders.
|
||||
- Structural updates rebuild grouped nodes only for projects whose local sessions, worktrees, repository state, or branch changed; unchanged project sections preserve references so memoized group/session descendants skip the update wave.
|
||||
- Empty successful lists, unresolved loads, and failed loads are separate UI states. Failed groups expose Retry and retain prior data.
|
||||
- Pins and folder assignments are not pruned from the first startup snapshot or from optimistic mutations. Confirmed local deletion and routed external deletion clean immediately; a later authoritative omission after an established baseline covers missed external delete events.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { deriveRecentSessions } from './activitySections';
|
||||
|
||||
const NOW = 200_000_000;
|
||||
const RECENT = NOW - (48 * 60 * 60 * 1000);
|
||||
const OLD = NOW - (72 * 60 * 60 * 1000);
|
||||
|
||||
const session = (id: string, options: { parentID?: string; archived?: number; updated?: number } = {}): Session => ({
|
||||
id,
|
||||
parentID: options.parentID,
|
||||
time: { created: OLD, updated: options.updated ?? OLD, archived: options.archived },
|
||||
} as Session);
|
||||
|
||||
describe('deriveRecentSessions', () => {
|
||||
test('includes an old root session while it is active', () => {
|
||||
const oldActive = session('old-active');
|
||||
|
||||
expect(deriveRecentSessions([oldActive], new Set([oldActive.id]), NOW)).toEqual([oldActive]);
|
||||
});
|
||||
|
||||
test('does not promote active children or archived sessions into Recent', () => {
|
||||
const child = session('child', { parentID: 'parent' });
|
||||
const archived = session('archived', { archived: NOW - 1 });
|
||||
|
||||
expect(deriveRecentSessions(
|
||||
[child, archived],
|
||||
new Set([child.id, archived.id]),
|
||||
NOW,
|
||||
)).toEqual([]);
|
||||
});
|
||||
|
||||
test('keeps inactive membership timestamp-based', () => {
|
||||
const oldSession = session('old');
|
||||
const recentSession = session('recent', { updated: RECENT });
|
||||
|
||||
expect(deriveRecentSessions([oldSession, recentSession], new Set(), NOW)).toEqual([recentSession]);
|
||||
});
|
||||
});
|
||||
@@ -22,20 +22,19 @@ const getSessionUpdatedAt = (session: Session): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Recent sessions are simply every non-archived, top-level session updated
|
||||
// within the last RECENT_SESSION_MAX_AGE_MS. No persisted history or live-busy
|
||||
// tracking: membership is timestamp-derived, while the caller applies shared
|
||||
// lifecycle ordering.
|
||||
// Recent contains non-archived root sessions that are active now or were
|
||||
// updated within the retention window. The caller applies shared lifecycle
|
||||
// ordering after this membership filter.
|
||||
export const deriveRecentSessions = (
|
||||
sessions: Session[],
|
||||
activeSessionIds: ReadonlySet<string>,
|
||||
now = Date.now(),
|
||||
): Session[] => {
|
||||
const minUpdatedAt = now - RECENT_SESSION_MAX_AGE_MS;
|
||||
const recent = sessions.filter((session) => {
|
||||
return sessions.filter((session) => {
|
||||
if (isArchivedSession(session) || isSubtaskSession(session)) {
|
||||
return false;
|
||||
}
|
||||
return getSessionUpdatedAt(session) >= minUpdatedAt;
|
||||
return activeSessionIds.has(session.id) || getSessionUpdatedAt(session) >= minUpdatedAt;
|
||||
});
|
||||
return recent;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user