import { useAtomValue } from "@effect/atom-react"; import * as Schema from "effect/Schema"; import { useEffect, useState, useSyncExternalStore, type CSSProperties, type ReactNode, } from "react"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { isElectron } from "../env"; import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalStorage"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings"; import { PanelAnimationSuppressionProvider, usePanelAnimationSettings, usePanelNavigationSuppression, } from "../panelAnimations"; import LegacyThreadSidebar from "./LegacySidebar"; import ThreadSidebar from "./Sidebar"; import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; import { SidebarChromeHeader } from "./sidebar/SidebarChrome"; import { resolveSidebarStageFocusRingOffsetClass, useSidebarStageBackdropVariant, } from "./SidebarStageBackdrop"; import { useProjects } from "../state/entities"; import { resolveInitialThreadSidebarWidth, resolveThreadSidebarMaximumWidth, THREAD_MAIN_CONTENT_MIN_WIDTH, THREAD_SIDEBAR_MIN_WIDTH, THREAD_SIDEBAR_WIDTH_STORAGE_KEY, } from "./threadSidebarWidth"; import { Sidebar, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar, useSidebarVisibility, } from "./ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; function subscribeToViewportWidth(onChange: () => void): () => void { window.addEventListener("resize", onChange); return () => window.removeEventListener("resize", onChange); } function readViewportWidth(): number { return window.innerWidth; } function readInitialThreadSidebarWidth(): number { try { return resolveInitialThreadSidebarWidth( getLocalStorageItem(THREAD_SIDEBAR_WIDTH_STORAGE_KEY, Schema.Finite), window.innerWidth, ); } catch (error) { console.error("Could not read persisted thread sidebar width.", error); return resolveInitialThreadSidebarWidth(null, window.innerWidth); } } function SidebarControl() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { toggleSidebar } = useSidebar(); const isSidebarVisible = useSidebarVisibility(); const environmentIdentificationMode = useEnvironmentIdentificationMode(); const stageBackdropVariant = useSidebarStageBackdropVariant( environmentIdentificationMode === "artwork", ); const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; if ( event.target instanceof HTMLElement && event.target.closest("[data-keybinding-capture]") ) { return; } if (resolveShortcutCommand(event, keybindings) !== "sidebar.toggle") return; event.preventDefault(); event.stopPropagation(); toggleSidebar(); }; // Capture before focused editors consume commands such as Mod+B for rich-text formatting. window.addEventListener("keydown", onKeyDown, true); return () => window.removeEventListener("keydown", onKeyDown, true); }, [keybindings, toggleSidebar]); return ( // The right-side layout controls carry mr-px (border compensation inside // the panel), so the trigger mirrors it: both clusters sit one extra pixel // off their edge and the titlebar reads symmetric.
} /> Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""}
); } // Settings swaps the thread sidebar out of the tree. Keep the lightweight // project projection subscribed so returning to a draft never renders the // zero-project state while the environment snapshot reconnects. function ProjectProjectionRetention() { useProjects(); return null; } export function AppSidebarLayout({ children }: { children: ReactNode }) { const navigate = useNavigate(); const legacySidebarEnabled = useLegacySidebarEnabled(); const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = usePanelAnimationSettings(); // Settings routes show the settings nav in place of whichever thread // sidebar is active. const pathname = useLocation({ select: (location) => location.pathname }); const panelAnimationsSuppressed = usePanelNavigationSuppression(pathname); const routePanelAnimationsActive = panelAnimationsActive && !panelAnimationsSuppressed; const isOnSettings = pathname === "/settings" || pathname.startsWith("/settings/"); const isMacosDesktop = isElectron && isMacPlatform(navigator.platform); const [sidebarWidth, setSidebarWidth] = useState(readInitialThreadSidebarWidth); // Subscribed rather than read once: the clamp must track live window size, // and a clamped drag ends with an unchanged width, which skips the re-render // that would otherwise refresh a render-time snapshot. const viewportWidth = useSyncExternalStore(subscribeToViewportWidth, readViewportWidth); const sidebarMaximumWidth = resolveThreadSidebarMaximumWidth(viewportWidth); const resetSidebarWidth = () => { try { removeLocalStorageItem(THREAD_SIDEBAR_WIDTH_STORAGE_KEY); } catch (error) { console.error("Could not clear persisted thread sidebar width.", error); } setSidebarWidth(resolveInitialThreadSidebarWidth(null, viewportWidth)); }; const [isWindowFullscreen, setIsWindowFullscreen] = useState(() => { const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState; return isMacosDesktop && typeof getWindowFullscreenState === "function" ? getWindowFullscreenState() : false; }); const sidebarProviderStyle = { "--sidebar-width": `${sidebarWidth}px`, "--panel-animation-duration": `${panelAnimationDurationMs}ms`, ...(isMacosDesktop && !isWindowFullscreen ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } : {}), } as CSSProperties; useEffect(() => { if (!isMacosDesktop) return; const bridge = window.desktopBridge; if (!bridge) return; const { getWindowFullscreenState, onWindowFullscreenStateChange } = bridge; if ( typeof getWindowFullscreenState !== "function" || typeof onWindowFullscreenStateChange !== "function" ) { return; } const unsubscribe = onWindowFullscreenStateChange(setIsWindowFullscreen); setIsWindowFullscreen(getWindowFullscreenState()); return unsubscribe; }, [isMacosDesktop]); useEffect(() => { const onMenuAction = window.desktopBridge?.onMenuAction; if (typeof onMenuAction !== "function") { return; } const unsubscribe = onMenuAction((action) => { if (action === "open-settings") { const isSettingsRoute = /^\/settings(\/|$)/.test(pathname); if (!isSettingsRoute) { void navigate({ to: "/settings" }); } } }); return () => { unsubscribe?.(); }; }, [navigate, pathname]); return ( nextWidth <= currentWidth || wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH, storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY, onResize: setSidebarWidth, }} > {isOnSettings ? ( <> ) : legacySidebarEnabled ? ( ) : ( )} {children} ); }