From 43f98e0344846d168e1ab2b5daf9a5d3d5204b72 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 3 May 2026 16:32:14 +0300 Subject: [PATCH] feat(ui): add recent section toggle --- .../src/components/session/SessionSidebar.tsx | 49 +++++++++++++------ .../session/sidebar/SidebarHeader.tsx | 10 ++++ packages/ui/src/lib/i18n/messages/en.ts | 1 + packages/ui/src/lib/i18n/messages/es.ts | 1 + packages/ui/src/lib/i18n/messages/ko.ts | 1 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 1 + packages/ui/src/lib/i18n/messages/uk.ts | 1 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 1 + .../ui/src/stores/useSessionDisplayStore.ts | 6 +++ 9 files changed, 55 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index 57b41092..87b747ed 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -58,6 +58,7 @@ import { } from './sidebar/ConfirmDialogs'; import { BulkActionBar } from './sidebar/BulkActionBar'; import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore'; +import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore'; import { type SessionGroup, type SessionNode } from './sidebar/types'; import { type ActiveNowEntry, @@ -996,19 +997,27 @@ export const SessionSidebar: React.FC = ({ return meta; }, [projectSections, homeDirectory]); - const activeNowSessions = React.useMemo( - () => deriveActiveNowSessions(activeNowEntries, new Map(sessions.map((session) => [session.id, session]))) - .sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds)), - [activeNowEntries, pinnedSessionIds, sessions], - ); + const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection); - const liveActiveSessions = React.useMemo( - () => deriveLiveActiveNowSessions(sessions, liveSessionStatuses), - [liveSessionStatuses, sessions], - ); + const activeNowSessions = React.useMemo(() => { + if (!showRecentSection) { + return []; + } + + return deriveActiveNowSessions(activeNowEntries, new Map(sessions.map((session) => [session.id, session]))) + .sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds)); + }, [activeNowEntries, pinnedSessionIds, sessions, showRecentSection]); + + const liveActiveSessions = React.useMemo(() => { + if (!showRecentSection) { + return []; + } + + return deriveLiveActiveNowSessions(sessions, liveSessionStatuses); + }, [liveSessionStatuses, sessions, showRecentSection]); React.useEffect(() => { - if (liveActiveSessions.length === 0) { + if (!showRecentSection || liveActiveSessions.length === 0) { return; } @@ -1020,9 +1029,13 @@ export const SessionSidebar: React.FC = ({ persistActiveNowEntries(safeStorage, next); return next; }); - }, [liveActiveSessions, safeStorage]); + }, [liveActiveSessions, safeStorage, showRecentSection]); React.useEffect(() => { + if (!showRecentSection) { + return; + } + const allKnownSessionsById = new Map(); [...sessions, ...archivedSessions].forEach((session) => { allKnownSessionsById.set(session.id, session); @@ -1035,11 +1048,15 @@ export const SessionSidebar: React.FC = ({ setActiveNowEntries(pruned); persistActiveNowEntries(safeStorage, pruned); - }, [activeNowEntries, archivedSessions, safeStorage, sessions]); + }, [activeNowEntries, archivedSessions, safeStorage, sessions, showRecentSection]); // Prefetch is wired below, after recentSessionIds is computed. const activitySections = React.useMemo(() => { + if (!showRecentSection) { + return []; + } + const toItem = (session: Session) => { const existing = sessionSidebarMetaById.get(session.id); const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null); @@ -1054,11 +1071,11 @@ export const SessionSidebar: React.FC = ({ return [ { key: 'active-now' as const, title: t('sessions.sidebar.activity.recentTitle'), items: activeNowSessions.map(toItem) }, ]; - }, [activeNowSessions, sessionSidebarMetaById, t]); + }, [activeNowSessions, sessionSidebarMetaById, showRecentSection, t]); const recentSessionIds = React.useMemo(() => { - return new Set(activitySections.flatMap((section) => section.items.map((item) => item.node.session.id))); - }, [activitySections]); + return new Set(activeNowSessions.map((session) => session.id)); + }, [activeNowSessions]); const recentSessionIdsList = React.useMemo(() => [...recentSessionIds], [recentSessionIds]); @@ -1412,7 +1429,7 @@ export const SessionSidebar: React.FC = ({ ], ); - const topContent = !hasSessionSearchQuery ? ( + const topContent = showRecentSection && !hasSessionSearchQuery ? ( state.displayMode); + const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection); const setDisplayMode = useSessionDisplayStore((state) => state.setDisplayMode); + const toggleRecentSection = useSessionDisplayStore((state) => state.toggleRecentSection); if (hideDirectoryControls) { return null; @@ -240,6 +242,14 @@ export function SidebarHeader(props: Props): React.ReactNode { {displayMode === 'minimal' ? : null} + + {t('sessions.sidebar.header.displayMode.showRecent')} + {showRecentSection ? : null} + + {t('sessions.sidebar.header.displayMode.collapseAll')} diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index f754245e..a7fe8533 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -180,6 +180,7 @@ export const dict = { 'sessions.sidebar.header.displayMode.label': 'Display mode', 'sessions.sidebar.header.displayMode.default': 'Default', 'sessions.sidebar.header.displayMode.minimal': 'Minimal', + 'sessions.sidebar.header.displayMode.showRecent': 'Show recent section', 'sessions.sidebar.header.displayMode.collapseAll': 'Collapse all', 'sessions.sidebar.header.displayMode.expandAll': 'Expand all', 'sessions.sidebar.header.search.matchCountSingle': '{count} match', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 3d48db64..a01e3e63 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -181,6 +181,7 @@ export const dict: Record = { "sessions.sidebar.header.displayMode.label": "Modo de visualización", "sessions.sidebar.header.displayMode.default": "Predeterminado", "sessions.sidebar.header.displayMode.minimal": "Mínimo", + "sessions.sidebar.header.displayMode.showRecent": "Mostrar recientes", "sessions.sidebar.header.displayMode.collapseAll": "Colapsar todo", "sessions.sidebar.header.displayMode.expandAll": "Expandir todo", "sessions.sidebar.header.search.matchCountSingle": "{count} coincidencia", diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 44d4d53d..98f4df6c 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -181,6 +181,7 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.label': '표시 모드', 'sessions.sidebar.header.displayMode.default': '기본값', 'sessions.sidebar.header.displayMode.minimal': '최소', + 'sessions.sidebar.header.displayMode.showRecent': '최근 섹션 표시', 'sessions.sidebar.header.displayMode.collapseAll': '모두 접기', 'sessions.sidebar.header.displayMode.expandAll': '모두 펼치기', 'sessions.sidebar.header.search.matchCountSingle': '{count}개 일치', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 3059aa70..e55f5144 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -181,6 +181,7 @@ export const dict: Record = { "sessions.sidebar.header.displayMode.label": "Modo de visualização", "sessions.sidebar.header.displayMode.default": "Padrão", "sessions.sidebar.header.displayMode.minimal": "Mínimo", + "sessions.sidebar.header.displayMode.showRecent": "Mostrar recentes", "sessions.sidebar.header.displayMode.collapseAll": "Recolher tudo", "sessions.sidebar.header.displayMode.expandAll": "Expandir tudo", "sessions.sidebar.header.search.matchCountSingle": "{count} correspondência", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 2176a499..1ea5e01e 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -181,6 +181,7 @@ export const dict: Record = { "sessions.sidebar.header.displayMode.label": "Режим відображення", "sessions.sidebar.header.displayMode.default": "За замовчуванням", "sessions.sidebar.header.displayMode.minimal": "Мінімальний", + "sessions.sidebar.header.displayMode.showRecent": "Показувати нещодавні", "sessions.sidebar.header.displayMode.collapseAll": "Згорнути все", "sessions.sidebar.header.displayMode.expandAll": "Розгорнути все", "sessions.sidebar.header.search.matchCountSingle": "{count} збіг", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 49a16899..ffce19df 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -181,6 +181,7 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.label': '显示模式', 'sessions.sidebar.header.displayMode.default': '默认', 'sessions.sidebar.header.displayMode.minimal': '精简', + 'sessions.sidebar.header.displayMode.showRecent': '显示最近部分', 'sessions.sidebar.header.displayMode.collapseAll': '全部折叠', 'sessions.sidebar.header.displayMode.expandAll': '全部展开', 'sessions.sidebar.header.search.matchCountSingle': '{count} 个匹配', diff --git a/packages/ui/src/stores/useSessionDisplayStore.ts b/packages/ui/src/stores/useSessionDisplayStore.ts index 66da8a59..2c706758 100644 --- a/packages/ui/src/stores/useSessionDisplayStore.ts +++ b/packages/ui/src/stores/useSessionDisplayStore.ts @@ -5,14 +5,20 @@ export type SessionDisplayMode = 'default' | 'minimal'; type SessionDisplayStore = { displayMode: SessionDisplayMode; + showRecentSection: boolean; setDisplayMode: (mode: SessionDisplayMode) => void; + setShowRecentSection: (show: boolean) => void; + toggleRecentSection: () => void; }; export const useSessionDisplayStore = create()( persist( (set) => ({ displayMode: 'default', + showRecentSection: true, setDisplayMode: (mode) => set({ displayMode: mode }), + setShowRecentSection: (show) => set({ showRecentSection: show }), + toggleRecentSection: () => set((state) => ({ showRecentSection: !state.showRecentSection })), }), { name: 'session-display-mode',