From 7cfc82e73ab2bb12b11b6fa86e060d5714dd44c7 Mon Sep 17 00:00:00 2001 From: Jair Escamilla Date: Wed, 9 Sep 2026 15:41:46 -0600 Subject: [PATCH 1/3] fix(sidebar): persist collapsed projects --- src/renderer/src/components/Sidebar.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 1f22c0b..554ad4b 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -60,8 +60,20 @@ const DEFAULT_WIDTH = 288 const SWEEP_MS = 30_000 const WIDTH_KEY = 'roxy.sidebar.width' const COLLAPSED_KEY = 'roxy.sidebar.collapsed' +const COLLAPSED_PROJECTS_KEY = 'roxy.sidebar.collapsedProjects' 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((path): path is string => typeof path === 'string') : [] + ) + } catch { + return new Set() + } +} + interface Project { path: string name: string @@ -169,7 +181,7 @@ 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)) @@ -194,6 +206,9 @@ export function Sidebar(): JSX.Element { useEffect(() => { localStorage.setItem(COLLAPSED_KEY, railed ? '1' : '0') }, [railed]) + useEffect(() => { + localStorage.setItem(COLLAPSED_PROJECTS_KEY, JSON.stringify([...collapsed])) + }, [collapsed]) // Double-click a session name to rename it inline. Enter / click-away saves, // Escape cancels. `cancelRef` lets the shared blur handler tell the two apart. From b3a5d477779221308e6ceecf66945c06c18fcc3a Mon Sep 17 00:00:00 2001 From: Jair Escamilla Date: Wed, 9 Sep 2026 16:00:01 -0600 Subject: [PATCH 2/3] fix(sidebar): address persistence review --- src/renderer/src/components/Sidebar.tsx | 51 +++++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 554ad4b..e82d657 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -59,21 +59,32 @@ const DEFAULT_WIDTH = 288 */ const SWEEP_MS = 30_000 const WIDTH_KEY = 'roxy.sidebar.width' -const COLLAPSED_KEY = 'roxy.sidebar.collapsed' -const COLLAPSED_PROJECTS_KEY = 'roxy.sidebar.collapsedProjects' +const RAIL_COLLAPSED_KEY = 'roxy.sidebar.collapsed' +const COLLAPSED_PROJECTS_KEY = 'roxy.sidebar.projects.v1' +const EXPANDED_SUBAGENTS_KEY = 'roxy.sidebar.subagents.v1' const clampWidth = (n: number): number => Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, n)) -const storedCollapsedProjects = (): Set => { +const storedSet = (key: string): Set => { try { - const paths: unknown = JSON.parse(localStorage.getItem(COLLAPSED_PROJECTS_KEY) ?? '[]') + const values: unknown = JSON.parse(localStorage.getItem(key) ?? '[]') return new Set( - Array.isArray(paths) ? paths.filter((path): path is string => typeof path === 'string') : [] + Array.isArray(values) + ? values.filter((value): value is string => typeof value === 'string') + : [] ) } catch { return new Set() } } +const storeSet = (key: string, values: Iterable): void => { + try { + localStorage.setItem(key, JSON.stringify([...values])) + } catch { + // Sidebar expansion is a preference, not a requirement. + } +} + interface Project { path: string name: string @@ -181,13 +192,17 @@ 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>(storedCollapsedProjects) - const [expandedSubs, setExpandedSubs] = useState>(new Set()) + const [collapsed, setCollapsed] = useState>(() => storedSet(COLLAPSED_PROJECTS_KEY)) + const [expandedSubs, setExpandedSubs] = useState>(() => + storedSet(EXPANDED_SUBAGENTS_KEY) + ) 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) @@ -204,11 +219,8 @@ 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]) - useEffect(() => { - localStorage.setItem(COLLAPSED_PROJECTS_KEY, JSON.stringify([...collapsed])) - }, [collapsed]) // Double-click a session name to rename it inline. Enter / click-away saves, // Escape cancels. `cancelRef` lets the shared blur handler tell the two apart. @@ -389,6 +401,14 @@ export function Sidebar(): JSX.Element { ) }, [chats, loops, projectOrder]) + useEffect(() => { + const live = new Set(projects.map((project) => project.path)) + storeSet( + COLLAPSED_PROJECTS_KEY, + [...collapsed].filter((path) => live.has(path)) + ) + }, [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. @@ -498,6 +518,13 @@ export function Sidebar(): JSX.Element { return map }, [chats]) + useEffect(() => { + storeSet( + EXPANDED_SUBAGENTS_KEY, + [...expandedSubs].filter((id) => subsByParent.has(id)) + ) + }, [expandedSubs, subsByParent]) + const toggleSubs = (id: string): void => setExpandedSubs((prev) => { const next = new Set(prev) From 897e7e2c64f041c40b869e27606f808df36443d7 Mon Sep 17 00:00:00 2001 From: Jair Escamilla Date: Thu, 10 Sep 2026 06:47:20 -0600 Subject: [PATCH 3/3] fix(sidebar): drop out-of-scope subagent persistence --- src/renderer/src/components/Sidebar.tsx | 44 ++++++++----------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index e82d657..bb3fe04 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -61,30 +61,19 @@ const SWEEP_MS = 30_000 const WIDTH_KEY = 'roxy.sidebar.width' const RAIL_COLLAPSED_KEY = 'roxy.sidebar.collapsed' const COLLAPSED_PROJECTS_KEY = 'roxy.sidebar.projects.v1' -const EXPANDED_SUBAGENTS_KEY = 'roxy.sidebar.subagents.v1' const clampWidth = (n: number): number => Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, n)) -const storedSet = (key: string): Set => { +const storedCollapsedProjects = (): Set => { try { - const values: unknown = JSON.parse(localStorage.getItem(key) ?? '[]') + const paths: unknown = JSON.parse(localStorage.getItem(COLLAPSED_PROJECTS_KEY) ?? '[]') return new Set( - Array.isArray(values) - ? values.filter((value): value is string => typeof value === 'string') - : [] + Array.isArray(paths) ? paths.filter((p): p is string => typeof p === 'string') : [] ) } catch { return new Set() } } -const storeSet = (key: string, values: Iterable): void => { - try { - localStorage.setItem(key, JSON.stringify([...values])) - } catch { - // Sidebar expansion is a preference, not a requirement. - } -} - interface Project { path: string name: string @@ -192,10 +181,8 @@ 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>(() => storedSet(COLLAPSED_PROJECTS_KEY)) - const [expandedSubs, setExpandedSubs] = useState>(() => - storedSet(EXPANDED_SUBAGENTS_KEY) - ) + 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 @@ -402,11 +389,15 @@ export function Sidebar(): JSX.Element { }, [chats, loops, projectOrder]) useEffect(() => { - const live = new Set(projects.map((project) => project.path)) - storeSet( - COLLAPSED_PROJECTS_KEY, - [...collapsed].filter((path) => live.has(path)) - ) + 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, @@ -518,13 +509,6 @@ export function Sidebar(): JSX.Element { return map }, [chats]) - useEffect(() => { - storeSet( - EXPANDED_SUBAGENTS_KEY, - [...expandedSubs].filter((id) => subsByParent.has(id)) - ) - }, [expandedSubs, subsByParent]) - const toggleSubs = (id: string): void => setExpandedSubs((prev) => { const next = new Set(prev)