From 5e5bab3da91e247036d1ac369dd42df6987fef6d Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 15:36:59 +0530 Subject: [PATCH] feat(renderer): persist last-opened folder (mc-file-store) + restore on mount --- src/renderer/components/layout/AppShell.tsx | 2 ++ src/renderer/hooks/use-restore-last-folder.ts | 18 +++++++++++++++++ src/renderer/stores/file-store.ts | 14 +++++++++++-- tests/unit/stores/file-store.test.ts | 20 +++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/renderer/hooks/use-restore-last-folder.ts diff --git a/src/renderer/components/layout/AppShell.tsx b/src/renderer/components/layout/AppShell.tsx index 5c13b2f..2242c8a 100644 --- a/src/renderer/components/layout/AppShell.tsx +++ b/src/renderer/components/layout/AppShell.tsx @@ -9,9 +9,11 @@ import { Sidebar } from '@/components/sidebar/Sidebar'; import { useAppStore } from '@/stores/app-store'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; import { useFileShortcuts } from '@/hooks/use-file-shortcuts'; +import { useRestoreLastFolder } from '@/hooks/use-restore-last-folder'; export function AppShell() { useFileShortcuts(); + useRestoreLastFolder(); const { sidebarVisible, previewVisible, paneSizes, setPaneSizes } = useAppStore(); return ( diff --git a/src/renderer/hooks/use-restore-last-folder.ts b/src/renderer/hooks/use-restore-last-folder.ts new file mode 100644 index 0000000..db62506 --- /dev/null +++ b/src/renderer/hooks/use-restore-last-folder.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { useFileStore } from '@/stores/file-store'; + +/** + * On mount, re-opens the last folder if one was persisted via the file store's + * zustand persist middleware. Idempotent — re-running with the same path is a + * no-op (openFolder's list IPC call would just re-fetch, which is acceptable). + */ +export function useRestoreLastFolder(): void { + useEffect(() => { + const { rootPath, openFolder, tree } = useFileStore.getState(); + if (rootPath && !tree) { + void openFolder(rootPath); + } + // Run once on mount only. Persist hydration happens before AppShell renders. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} diff --git a/src/renderer/stores/file-store.ts b/src/renderer/stores/file-store.ts index dc01353..c2bf7d7 100644 --- a/src/renderer/stores/file-store.ts +++ b/src/renderer/stores/file-store.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { enableMapSet } from 'immer'; import { ipc } from '@/lib/ipc'; @@ -68,7 +69,8 @@ function updateNode(tree: FileNode, dirPath: string, updater: (node: FileNode) = } export const useFileStore = create()( - immer((set) => ({ + persist( + immer((set) => ({ tree: null, rootPath: null, expanded: new Set(), @@ -221,5 +223,13 @@ export const useFileStore = create()( useFileStore.getState().markTabClean(activeTabId); return true; }, - })) + })), + { + name: 'mc-file-store', + storage: createJSONStorage(() => localStorage), + // Only persist the folder path; tree, expanded Set, and openTabs are + // rebuilt on demand. The app re-opens the folder on startup. + partialize: (state) => ({ rootPath: state.rootPath }), + } + ) ); diff --git a/tests/unit/stores/file-store.test.ts b/tests/unit/stores/file-store.test.ts index f07fdf1..184b498 100644 --- a/tests/unit/stores/file-store.test.ts +++ b/tests/unit/stores/file-store.test.ts @@ -396,4 +396,24 @@ describe('useFileStore', () => { expect(useEditorStore.getState().buffers.get('/root/doc.md')!.dirty).toBe(true); expect(useFileStore.getState().openTabs[0].dirty).toBe(true); }); + + // --- persistence --- + + it('persists rootPath to localStorage and restores on next mount', async () => { + fakeList.mockResolvedValue({ + ok: true, + data: [{ name: 'a.md', path: '/root/a.md', isDirectory: false }], + }); + await useFileStore.getState().openFolder('/root'); + + // Force the persist middleware to flush to localStorage + await useFileStore.persist?.flush?.(); + + const stored = JSON.parse(localStorage.getItem('mc-file-store') ?? '{}'); + expect(stored.state.rootPath).toBe('/root'); + // Ensure non-persisted fields are NOT in storage + expect(stored.state.tree).toBeUndefined(); + expect(stored.state.openTabs).toBeUndefined(); + expect(stored.state.expanded).toBeUndefined(); + }); });