diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 1f22c0b..bb3fe04 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -59,9 +59,21 @@ const DEFAULT_WIDTH = 288 */ const SWEEP_MS = 30_000 const WIDTH_KEY = 'roxy.sidebar.width' -const COLLAPSED_KEY = 'roxy.sidebar.collapsed' +const RAIL_COLLAPSED_KEY = 'roxy.sidebar.collapsed' +const COLLAPSED_PROJECTS_KEY = 'roxy.sidebar.projects.v1' const clampWidth = (n: number): number => Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, n)) +const storedCollapsedProjects = (): Set => { + try { + const paths: unknown = JSON.parse(localStorage.getItem(COLLAPSED_PROJECTS_KEY) ?? '[]') + return new Set( + Array.isArray(paths) ? paths.filter((p): p is string => typeof p === 'string') : [] + ) + } catch { + return new Set() + } +} + interface Project { path: string name: string @@ -169,13 +181,15 @@ export function Sidebar(): JSX.Element { const reorderSessions = useRoxyStore((s) => s.reorderSessions) const reorderProjects = useRoxyStore((s) => s.reorderProjects) const projectOrder = useRoxyStore((s) => s.projectOrder) - const [collapsed, setCollapsed] = useState>(new Set()) + const [collapsed, setCollapsed] = useState>(storedCollapsedProjects) const [expandedSubs, setExpandedSubs] = useState>(new Set()) const [width, setWidth] = useState(() => { const v = Number(localStorage.getItem(WIDTH_KEY)) return Number.isFinite(v) && v >= MIN_WIDTH && v <= MAX_WIDTH ? v : DEFAULT_WIDTH }) - const [railed, setRailed] = useState(() => localStorage.getItem(COLLAPSED_KEY) === '1') + const [railed, setRailed] = useState( + () => localStorage.getItem(RAIL_COLLAPSED_KEY) === '1' + ) // The open right-click menu: which session, and where the cursor was. const [contextMenu, setContextMenu] = useState<{ chat: Chat; x: number; y: number } | null>(null) const [remoteOpen, setRemoteOpen] = useState(false) @@ -192,7 +206,7 @@ export function Sidebar(): JSX.Element { localStorage.setItem(WIDTH_KEY, String(width)) }, [width]) useEffect(() => { - localStorage.setItem(COLLAPSED_KEY, railed ? '1' : '0') + localStorage.setItem(RAIL_COLLAPSED_KEY, railed ? '1' : '0') }, [railed]) // Double-click a session name to rename it inline. Enter / click-away saves, @@ -374,6 +388,18 @@ export function Sidebar(): JSX.Element { ) }, [chats, loops, projectOrder]) + useEffect(() => { + const live = new Set(projects.map((p) => p.path)) + try { + localStorage.setItem( + COLLAPSED_PROJECTS_KEY, + JSON.stringify([...collapsed].filter((p) => live.has(p))) + ) + } catch { + // Sidebar expansion is a preference, not a requirement. + } + }, [collapsed, projects]) + // Reorder projects so the dragged folder lands before/after the drop target, // then persist. Only real folders take part — the '(no folder)' catch-all // isn't a registered project, so it always stays pinned at the bottom.