Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion components/SessionSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useLayoutEffect, useState, useCallback, useRef, type CSSProperties, type ReactNode } from "react";
import type { SessionInfo } from "@/lib/types";
import { loadExplorerOpen, saveExplorerOpen } from "@/lib/file-explorer-state";
import { useI18n } from "@/hooks/useI18n";
import { DirectoryPicker } from "./DirectoryPicker";
import { FileExplorer, type FileExplorerHandle } from "./FileExplorer";
Expand Down Expand Up @@ -465,6 +466,12 @@ export function SessionSidebar({ selectedSessionId, onSelectSession, onNewSessio
loadSessions(isFirst);
}, [loadSessions, refreshKey]);

// Browser storage is unavailable during server rendering. Restore the panel
// preference after hydration so a collapsed explorer stays collapsed on reload.
useEffect(() => {
setExplorerOpen(loadExplorerOpen());
}, []);

// Persist unread markers so they survive a browser refresh before the user
// has actually opened the completed session.
useEffect(() => {
Expand Down Expand Up @@ -1558,7 +1565,11 @@ export function SessionSidebar({ selectedSessionId, onSelectSession, onNewSessio
>
<div style={{ display: "flex", alignItems: "center", flexShrink: 0 }}>
<button
onClick={() => setExplorerOpen((v) => !v)}
onClick={() => setExplorerOpen((open) => {
const next = !open;
saveExplorerOpen(next);
return next;
})}
style={{
display: "flex",
alignItems: "center",
Expand Down
43 changes: 43 additions & 0 deletions lib/file-explorer-state.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createJiti } from "jiti";

const jiti = createJiti(import.meta.url);
const { loadExplorerOpen, saveExplorerOpen } = await jiti.import("./file-explorer-state.ts");

function createStorage(initial = {}) {
const values = new Map(Object.entries(initial));
return {
values,
getItem(key) {
return values.get(key) ?? null;
},
setItem(key, value) {
values.set(key, value);
},
};
}

test("defaults to an open file explorer", () => {
assert.equal(loadExplorerOpen(createStorage()), true);
});

test("saves and restores the file explorer panel state", () => {
const storage = createStorage();

saveExplorerOpen(false, storage);
assert.equal(loadExplorerOpen(storage), false);

saveExplorerOpen(true, storage);
assert.equal(loadExplorerOpen(storage), true);
});

test("falls back to open when browser storage is unavailable", () => {
const unavailable = {
getItem() { throw new Error("blocked"); },
setItem() { throw new Error("blocked"); },
};

assert.equal(loadExplorerOpen(unavailable), true);
assert.doesNotThrow(() => saveExplorerOpen(false, unavailable));
});
32 changes: 32 additions & 0 deletions lib/file-explorer-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const EXPLORER_OPEN_STORAGE_KEY = "pi-web:file-explorer:open";

interface StorageLike {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
}

function getBrowserStorage(): StorageLike | null {
if (typeof window === "undefined") return null;
return window.localStorage;
}

export function loadExplorerOpen(storage: StorageLike | null = getBrowserStorage()): boolean {
if (!storage) return true;
try {
return storage.getItem(EXPLORER_OPEN_STORAGE_KEY) !== "false";
} catch {
return true;
}
}

export function saveExplorerOpen(
open: boolean,
storage: StorageLike | null = getBrowserStorage(),
): void {
if (!storage) return;
try {
storage.setItem(EXPLORER_OPEN_STORAGE_KEY, String(open));
} catch {
// Persistence is best-effort; privacy mode and storage quotas must not break the explorer.
}
}