feat(renderer): persist last-opened folder (mc-file-store) + restore on mount

This commit is contained in:
2026-06-05 15:36:59 +05:30
parent ce2bf62d42
commit 5e5bab3da9
4 changed files with 52 additions and 2 deletions
@@ -9,9 +9,11 @@ import { Sidebar } from '@/components/sidebar/Sidebar';
import { useAppStore } from '@/stores/app-store'; import { useAppStore } from '@/stores/app-store';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import { useFileShortcuts } from '@/hooks/use-file-shortcuts'; import { useFileShortcuts } from '@/hooks/use-file-shortcuts';
import { useRestoreLastFolder } from '@/hooks/use-restore-last-folder';
export function AppShell() { export function AppShell() {
useFileShortcuts(); useFileShortcuts();
useRestoreLastFolder();
const { sidebarVisible, previewVisible, paneSizes, setPaneSizes } = useAppStore(); const { sidebarVisible, previewVisible, paneSizes, setPaneSizes } = useAppStore();
return ( return (
@@ -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
}, []);
}
+12 -2
View File
@@ -1,4 +1,5 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { enableMapSet } from 'immer'; import { enableMapSet } from 'immer';
import { ipc } from '@/lib/ipc'; import { ipc } from '@/lib/ipc';
@@ -68,7 +69,8 @@ function updateNode(tree: FileNode, dirPath: string, updater: (node: FileNode) =
} }
export const useFileStore = create<FileState>()( export const useFileStore = create<FileState>()(
immer((set) => ({ persist(
immer((set) => ({
tree: null, tree: null,
rootPath: null, rootPath: null,
expanded: new Set<string>(), expanded: new Set<string>(),
@@ -221,5 +223,13 @@ export const useFileStore = create<FileState>()(
useFileStore.getState().markTabClean(activeTabId); useFileStore.getState().markTabClean(activeTabId);
return true; 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 }),
}
)
); );
+20
View File
@@ -396,4 +396,24 @@ describe('useFileStore', () => {
expect(useEditorStore.getState().buffers.get('/root/doc.md')!.dirty).toBe(true); expect(useEditorStore.getState().buffers.get('/root/doc.md')!.dirty).toBe(true);
expect(useFileStore.getState().openTabs[0].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();
});
}); });