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. <div className="pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 ml-px flex h-[var(--workspace-topbar-height)] items-center" data-sidebar-control="" > <Tooltip> <TooltipTrigger render={ <SidebarTrigger className={cn( "pointer-events-auto", isSidebarVisible && stageBackdropVariant && "focus-visible:ring-white/90 [&_svg]:stroke-white/90! [&_svg]:opacity-100! [&_svg]:hover:stroke-white! [:hover,[data-pressed]]:bg-white/15", isSidebarVisible && stageBackdropVariant && resolveSidebarStageFocusRingOffsetClass(stageBackdropVariant), )} aria-label="Toggle main sidebar" /> } /> <TooltipPopup side="bottom"> Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""} </TooltipPopup> </Tooltip> </div> );} // 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 ( <PanelAnimationSuppressionProvider value={panelAnimationsSuppressed}> <SidebarProvider className="h-dvh! min-h-0!" data-panel-animations={routePanelAnimationsActive ? "true" : "false"} defaultOpen style={sidebarProviderStyle} > <ProjectProjectionRetention /> <Sidebar side="left" collapsible="offcanvas" data-app-sidebar="" className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground" resizable={{ maxWidth: sidebarMaximumWidth, minWidth: THREAD_SIDEBAR_MIN_WIDTH, shouldAcceptWidth: ({ currentWidth, nextWidth, wrapper }) => nextWidth <= currentWidth || wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH, storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY, onResize: setSidebarWidth, }} > {isOnSettings ? ( <> <SidebarChromeHeader isElectron={isElectron} /> <SettingsSidebarNav pathname={pathname} /> </> ) : legacySidebarEnabled ? ( <LegacyThreadSidebar /> ) : ( <ThreadSidebar /> )} <SidebarRail onDoubleClick={resetSidebarWidth} /> </Sidebar> {children} <SidebarControl /> </SidebarProvider> </PanelAnimationSuppressionProvider> );}
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.
<div
className="pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 ml-px flex h-[var(--workspace-topbar-height)] items-center"
data-sidebar-control=""
>
<Tooltip>
<TooltipTrigger
render={
<SidebarTrigger
className={cn(
"pointer-events-auto",
isSidebarVisible &&
stageBackdropVariant &&
"focus-visible:ring-white/90 [&_svg]:stroke-white/90! [&_svg]:opacity-100! [&_svg]:hover:stroke-white! [:hover,[data-pressed]]:bg-white/15",
isSidebarVisible &&
stageBackdropVariant &&
resolveSidebarStageFocusRingOffsetClass(stageBackdropVariant),
)}
aria-label="Toggle main sidebar"
/>
}
/>
<TooltipPopup side="bottom">
Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""}
</TooltipPopup>
</Tooltip>
</div>
);
}
// 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 (
<PanelAnimationSuppressionProvider value={panelAnimationsSuppressed}>
<SidebarProvider
className="h-dvh! min-h-0!"
data-panel-animations={routePanelAnimationsActive ? "true" : "false"}
defaultOpen
style={sidebarProviderStyle}
>
<ProjectProjectionRetention />
<Sidebar
side="left"
collapsible="offcanvas"
data-app-sidebar=""
className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
resizable={{
maxWidth: sidebarMaximumWidth,
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
shouldAcceptWidth: ({ currentWidth, nextWidth, wrapper }) =>
nextWidth <= currentWidth ||
wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH,
storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,
onResize: setSidebarWidth,
}}
>
{isOnSettings ? (
<>
<SidebarChromeHeader isElectron={isElectron} />
<SettingsSidebarNav pathname={pathname} />
</>
) : legacySidebarEnabled ? (
<LegacyThreadSidebar />
) : (
<ThreadSidebar />
)}
<SidebarRail onDoubleClick={resetSidebarWidth} />
</Sidebar>
{children}
<SidebarControl />
</SidebarProvider>
</PanelAnimationSuppressionProvider>
);
}