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 } from "../hooks/useLocalStorage";
import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
import { cn, isMacPlatform } from "../lib/utils";
import { primaryServerKeybindingsAtom } from "../state/server";
import { useClientSettings } from "../hooks/useSettings";
import ThreadSidebar from "./Sidebar";
import ThreadSidebarV2 from "./SidebarV2";
import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop";
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 stageBackdropVariant = useSidebarStageBackdropVariant();
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 (
}
/>
Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""}
);
}
export function AppSidebarLayout({ children }: { children: ReactNode }) {
const navigate = useNavigate();
const sidebarV2Enabled = useClientSettings((settings) => settings.sidebarV2Enabled);
// Settings routes render the settings nav, which lives in the v1 component
// and is identical for both sidebars — so v1 stays mounted there.
const pathname = useLocation({ select: (location) => location.pathname });
const isOnSettings = pathname === "/settings" || pathname.startsWith("/settings/");
const useSidebarV2 = sidebarV2Enabled && !isOnSettings;
const useSidebarV2Theme = useSidebarV2 || isOnSettings;
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 [isWindowFullscreen, setIsWindowFullscreen] = useState(() => {
const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState;
return isMacosDesktop && typeof getWindowFullscreenState === "function"
? getWindowFullscreenState()
: false;
});
const sidebarProviderStyle = {
"--sidebar-width": `${sidebarWidth}px`,
...(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,
}}
>
{useSidebarV2 ? : }
{children}
);
}