refactor(recent): replace active-now tracking with recent session window
Replace persisted 'active now' tracking with 48-hour recency window Remove Zustand ActiveNowStore and localStorage persistence Simplify session sidebar data flow
This commit is contained in:
@@ -42,29 +42,6 @@ export function SidebarActivitySections({
|
||||
const [visibleCountBySection, setVisibleCountBySection] = React.useState<Map<string, number>>(new Map());
|
||||
const flatVariant = variant === 'flat';
|
||||
|
||||
const toggleSection = React.useCallback((key: string) => {
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const showMoreSessions = React.useCallback((key: string, currentVisibleCount: number, totalCount: number) => {
|
||||
setVisibleCountBySection((prev) => {
|
||||
const nextVisibleCount = flatVariant
|
||||
? Math.min(totalCount, currentVisibleCount + batchSize)
|
||||
: totalCount;
|
||||
const next = new Map(prev);
|
||||
next.set(key, nextVisibleCount);
|
||||
return next;
|
||||
});
|
||||
}, [batchSize, flatVariant]);
|
||||
|
||||
const resetSectionLimit = React.useCallback((key: string) => {
|
||||
setVisibleCountBySection((prev) => {
|
||||
if (!prev.has(key)) {
|
||||
@@ -76,6 +53,30 @@ export function SidebarActivitySections({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleSection = React.useCallback((key: string) => {
|
||||
// Collapsing/expanding resets any "show more" batches, matching the
|
||||
// worktree/project group behavior.
|
||||
resetSectionLimit(key);
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [resetSectionLimit]);
|
||||
|
||||
const showMoreSessions = React.useCallback((key: string, currentVisibleCount: number, totalCount: number) => {
|
||||
setVisibleCountBySection((prev) => {
|
||||
const nextVisibleCount = Math.min(totalCount, currentVisibleCount + batchSize);
|
||||
const next = new Map(prev);
|
||||
next.set(key, nextVisibleCount);
|
||||
return next;
|
||||
});
|
||||
}, [batchSize]);
|
||||
|
||||
const visibleSections = sections.filter((section) => section.items.length > 0);
|
||||
if (visibleSections.length === 0) {
|
||||
return null;
|
||||
@@ -132,9 +133,7 @@ export function SidebarActivitySections({
|
||||
onClick={() => showMoreSessions(section.key, visibleItems.length, section.items.length)}
|
||||
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
|
||||
>
|
||||
{remainingCount === 1
|
||||
? t('sessions.sidebar.group.showMoreSingle', { count: remainingCount })
|
||||
: t('sessions.sidebar.group.showMorePlural', { count: remainingCount })}
|
||||
{t('sessions.sidebar.group.showMore')}
|
||||
</button>
|
||||
) : null}
|
||||
{canShowFewer ? (
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import type { SessionStatus } from '@opencode-ai/sdk/v2/client';
|
||||
|
||||
export const ACTIVE_NOW_STORAGE_KEY = 'oc.sessions.activeNow';
|
||||
export const ACTIVE_NOW_MAX_AGE_MS = 36 * 60 * 60 * 1000;
|
||||
|
||||
export type ActiveNowEntry = {
|
||||
sessionId: string;
|
||||
};
|
||||
export const RECENT_SESSION_MAX_AGE_MS = 48 * 60 * 60 * 1000;
|
||||
|
||||
const isSubtaskSession = (session: Session): boolean => {
|
||||
return Boolean((session as Session & { parentID?: string | null }).parentID);
|
||||
@@ -28,97 +22,25 @@ const getSessionUpdatedAt = (session: Session): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const readActiveNowEntries = (storage: Storage): ActiveNowEntry[] => {
|
||||
try {
|
||||
const raw = storage.getItem(ACTIVE_NOW_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const next: ActiveNowEntry[] = [];
|
||||
parsed.forEach((item) => {
|
||||
const sessionId = typeof item === 'string'
|
||||
? item
|
||||
: (item && typeof item === 'object' && 'sessionId' in item && typeof item.sessionId === 'string' ? item.sessionId : null);
|
||||
if (!sessionId || seen.has(sessionId)) {
|
||||
return;
|
||||
}
|
||||
seen.add(sessionId);
|
||||
next.push({ sessionId });
|
||||
});
|
||||
return next;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const persistActiveNowEntries = (storage: Storage, entries: ActiveNowEntry[]): void => {
|
||||
try {
|
||||
storage.setItem(ACTIVE_NOW_STORAGE_KEY, JSON.stringify(entries));
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
};
|
||||
|
||||
export const pruneActiveNowEntries = (
|
||||
entries: ActiveNowEntry[],
|
||||
sessionsById: Map<string, Session>,
|
||||
now = Date.now(),
|
||||
): ActiveNowEntry[] => {
|
||||
const minUpdatedAt = now - ACTIVE_NOW_MAX_AGE_MS;
|
||||
return entries.filter((entry) => {
|
||||
const session = sessionsById.get(entry.sessionId);
|
||||
if (!session) {
|
||||
return true;
|
||||
}
|
||||
if (isArchivedSession(session)) {
|
||||
return false;
|
||||
}
|
||||
return getSessionUpdatedAt(session) >= minUpdatedAt;
|
||||
});
|
||||
};
|
||||
|
||||
export const addActiveNowSession = (entries: ActiveNowEntry[], sessionId: string): ActiveNowEntry[] => {
|
||||
if (!sessionId || entries.some((entry) => entry.sessionId === sessionId)) {
|
||||
return entries;
|
||||
}
|
||||
return [{ sessionId }, ...entries];
|
||||
};
|
||||
|
||||
export const sortSessionsByUpdated = (sessions: Session[]): Session[] => {
|
||||
return [...sessions].sort((a, b) => getSessionUpdatedAt(b) - getSessionUpdatedAt(a));
|
||||
};
|
||||
|
||||
export const deriveActiveNowSessions = (
|
||||
entries: ActiveNowEntry[],
|
||||
sessionsById: Map<string, Session>,
|
||||
): Session[] => {
|
||||
const sessions = entries
|
||||
.map((entry) => sessionsById.get(entry.sessionId) ?? null)
|
||||
.filter((session): session is Session => Boolean(session))
|
||||
.filter((session) => !isArchivedSession(session))
|
||||
.filter((session) => !isSubtaskSession(session));
|
||||
return sortSessionsByUpdated(sessions);
|
||||
};
|
||||
|
||||
export const deriveLiveActiveNowSessions = (
|
||||
// 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 derived directly from session timestamps.
|
||||
export const deriveRecentSessions = (
|
||||
sessions: Session[],
|
||||
statuses: Record<string, SessionStatus>,
|
||||
now = Date.now(),
|
||||
): Session[] => {
|
||||
const activeSessions = sessions.filter((session) => {
|
||||
const minUpdatedAt = now - RECENT_SESSION_MAX_AGE_MS;
|
||||
const recent = sessions.filter((session) => {
|
||||
if (isArchivedSession(session) || isSubtaskSession(session)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = statuses[session.id];
|
||||
return status?.type === 'busy' || status?.type === 'retry';
|
||||
return getSessionUpdatedAt(session) >= minUpdatedAt;
|
||||
});
|
||||
|
||||
return sortSessionsByUpdated(activeSessions);
|
||||
return sortSessionsByUpdated(recent);
|
||||
};
|
||||
|
||||
export const getSessionUpdatedAtMs = getSessionUpdatedAt;
|
||||
|
||||
Reference in New Issue
Block a user