authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-08-11 03:32:22+00:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-10 23:32:22-04:00
log59d0c922e9363b209bcb004444c05e70fa9fdd8b
treefe56b2bdaced47a2e179b3f9d609e1ae817f4cf2
parent2ea51bd31f494ec3214a0c9f9adc3262c71e6530
signature Signed by PGP key B5690EEEBB952194

fix(web): persist sidebar shelf collapse state (#5136)

Co-authored-by: Illia Panasenko <hello@ipanasenko.me>

1 files changed, 23 insertions(+), 4 deletions(-)

apps/web/src/components/Sidebar.tsx+23-4
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { autoAnimate } from "@formkit/auto-animate";1import { autoAnimate } from "@formkit/auto-animate";
2import { useAtomValue } from "@effect/atom-react";2import { useAtomValue } from "@effect/atom-react";
3import * as Schema from "effect/Schema";
3import {4import {
4 DndContext,5 DndContext,
5 PointerSensor,6 PointerSensor,
...@@ -100,6 +101,7 @@ import { openCommandPalette } from "../commandPaletteBus";...@@ -100,6 +101,7 @@ import { openCommandPalette } from "../commandPaletteBus";
100import { startNewThreadFromContext } from "../lib/chatThreadActions";101import { startNewThreadFromContext } from "../lib/chatThreadActions";
101import { useClientSettings } from "../hooks/useSettings";102import { useClientSettings } from "../hooks/useSettings";
102import { useCopyToClipboard } from "../hooks/useCopyToClipboard";103import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
104import { useLocalStorage } from "../hooks/useLocalStorage";
103import { useNowMinute } from "../hooks/useNowMinute";105import { useNowMinute } from "../hooks/useNowMinute";
104import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";106import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
105import { useProjects, useThreadShells } from "../state/entities";107import { useProjects, useThreadShells } from "../state/entities";
...@@ -176,6 +178,9 @@ import {...@@ -176,6 +178,9 @@ import {
176// stays behind an explicit Show more.178// stays behind an explicit Show more.
177const SETTLED_TAIL_INITIAL_COUNT = 10;179const SETTLED_TAIL_INITIAL_COUNT = 10;
178const SETTLED_TAIL_PAGE_COUNT = 25;180const SETTLED_TAIL_PAGE_COUNT = 25;
181// Keep the v2 key so existing preferences survive the v2-to-default rename.
182const SETTLED_SHELF_EXPANDED_KEY = "t3code:sidebar-v2:settled-expanded";
183const SNOOZED_SHELF_EXPANDED_KEY = "t3code:sidebar-v2:snoozed-expanded";
179184
180function compactSidebarTimeLabel(label: string): string {185function compactSidebarTimeLabel(label: string): string {
181 if (label === "just now") return "now";186 if (label === "just now") return "now";
...@@ -2044,8 +2049,15 @@ export default function Sidebar() {...@@ -2044,8 +2049,15 @@ export default function Sidebar() {
2044 () => setSettledVisibleCount((count) => count + SETTLED_TAIL_PAGE_COUNT),2049 () => setSettledVisibleCount((count) => count + SETTLED_TAIL_PAGE_COUNT),
2045 [],2050 [],
2046 );2051 );
2047 const [settledShelfExpanded, setSettledShelfExpanded] = useState(true);2052 const [settledShelfExpanded, setSettledShelfExpanded] = useLocalStorage(
2048 const toggleSettledShelf = useCallback(() => setSettledShelfExpanded((value) => !value), []);2053 SETTLED_SHELF_EXPANDED_KEY,
2054 true,
2055 Schema.Boolean,
2056 );
2057 const toggleSettledShelf = useCallback(
2058 () => setSettledShelfExpanded((value) => !value),
2059 [setSettledShelfExpanded],
2060 );
2049 const renderedSettledThreads = useMemo(() => {2061 const renderedSettledThreads = useMemo(() => {
2050 if (settledShelfExpanded) return visibleSettledThreads;2062 if (settledShelfExpanded) return visibleSettledThreads;
2051 if (routeThreadKey === null) return [];2063 if (routeThreadKey === null) return [];
...@@ -2059,8 +2071,15 @@ export default function Sidebar() {...@@ -2059,8 +2071,15 @@ export default function Sidebar() {
2059 // The snoozed shelf is collapsed by default: out of the way, never gone.2071 // The snoozed shelf is collapsed by default: out of the way, never gone.
2060 // Collapsed threads don't render (and so don't participate in jump2072 // Collapsed threads don't render (and so don't participate in jump
2061 // shortcuts or multi-select), matching the settled tail's paging model.2073 // shortcuts or multi-select), matching the settled tail's paging model.
2062 const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false);2074 const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useLocalStorage(
2063 const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []);2075 SNOOZED_SHELF_EXPANDED_KEY,
2076 false,
2077 Schema.Boolean,
2078 );
2079 const toggleSnoozedShelf = useCallback(
2080 () => setSnoozedShelfExpanded((value) => !value),
2081 [setSnoozedShelfExpanded],
2082 );
2064 const visibleSnoozedThreads = useMemo(() => {2083 const visibleSnoozedThreads = useMemo(() => {
2065 if (snoozedShelfExpanded) return snoozedThreads;2084 if (snoozedShelfExpanded) return snoozedThreads;
2066 // The open thread must never vanish behind the collapsed shelf: a2085 // The open thread must never vanish behind the collapsed shelf: a