Tree panel mode lives in the root layout: no open-then-close on session open
The persisted mode (lh:tree-panel-mode) was read inside TreePanel — a per-page component that remounts on every session navigation. First paint rendered the SSR-safe "list" default, then the mount effect applied the stored mode: with the animated frame, opening a session visibly opened the panel and then closed it. The mode now lives in a root-layout provider (the SidebarProvider pattern) and survives navigations untouched — the panel mounts already in the right mode. The default stays "list" so SSR and first client render agree; the stored preference applies once, on provider mount.
This commit is contained in:
+4
-1
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { AppShell, SidebarProvider } from "@/components/sidebar";
|
import { AppShell, SidebarProvider } from "@/components/sidebar";
|
||||||
|
import { TreePanelModeProvider } from "@/components/tree-panel";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Learning Harness",
|
title: "Learning Harness",
|
||||||
@@ -13,7 +14,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<html lang="en" className="dark">
|
<html lang="en" className="dark">
|
||||||
<body className="min-h-dvh antialiased">
|
<body className="min-h-dvh antialiased">
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppShell>{children}</AppShell>
|
<TreePanelModeProvider>
|
||||||
|
<AppShell>{children}</AppShell>
|
||||||
|
</TreePanelModeProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+32
-12
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronsRightIcon,
|
ChevronsRightIcon,
|
||||||
@@ -166,16 +166,25 @@ export function nextNodeId(
|
|||||||
|
|
||||||
// ─── panel ───────────────────────────────────────────────────────────────────
|
// ─── panel ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function TreePanel({
|
/**
|
||||||
messages,
|
* The panel mode lives in the ROOT layout, not in TreePanel. TreePanel is
|
||||||
}: {
|
* per-page (chat sessions remount it on every navigation), so a local copy
|
||||||
messages: readonly TreePanelMessage[];
|
* would paint its default first and then "correct" to the stored mode —
|
||||||
}) {
|
* with the animated frame, opening a session visibly opened-then-closed
|
||||||
const tree = useMemo(() => latestTree(messages), [messages]);
|
* the panel. In the root layout the mode survives navigations untouched,
|
||||||
|
* exactly like the left sidebar's open state (SidebarProvider).
|
||||||
|
*/
|
||||||
|
const TreePanelModeContext = createContext<{ mode: PanelMode; go: (next: PanelMode) => void }>({
|
||||||
|
mode: "list",
|
||||||
|
go: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
// Start in the list so server and client first-render agree (no
|
/** Root-layout persistence of the panel mode (rail | list | graph). */
|
||||||
// hydration mismatch); the stored preference is applied after mount.
|
export function TreePanelModeProvider({ children }: { children: React.ReactNode }) {
|
||||||
// Same pattern as the sidebar.
|
// SSR default: "list" — server and client first-render agree (no
|
||||||
|
// hydration mismatch). The stored preference applies on mount, and
|
||||||
|
// the provider itself never remounts, so after that first correction
|
||||||
|
// the mode is simply THERE on every session you open.
|
||||||
const [mode, setMode] = useState<PanelMode>("list");
|
const [mode, setMode] = useState<PanelMode>("list");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -193,14 +202,25 @@ export function TreePanel({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const go = (next: PanelMode) => {
|
const go = useCallback((next: PanelMode) => {
|
||||||
setMode(next);
|
setMode(next);
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(MODE_KEY, next);
|
window.localStorage.setItem(MODE_KEY, next);
|
||||||
} catch {
|
} catch {
|
||||||
// see above
|
// see above
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
return <TreePanelModeContext.Provider value={{ mode, go }}>{children}</TreePanelModeContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TreePanel({
|
||||||
|
messages,
|
||||||
|
}: {
|
||||||
|
messages: readonly TreePanelMessage[];
|
||||||
|
}) {
|
||||||
|
const tree = useMemo(() => latestTree(messages), [messages]);
|
||||||
|
const { mode, go } = useContext(TreePanelModeContext);
|
||||||
|
|
||||||
// Empty state: no tree yet → the panel does not render at all. It appears
|
// Empty state: no tree yet → the panel does not render at all. It appears
|
||||||
// the moment plan_init's output lands and updates live from there.
|
// the moment plan_init's output lands and updates live from there.
|
||||||
|
|||||||
Reference in New Issue
Block a user