Download src/components/features/sidebar/sidebar.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/sidebar/sidebar.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/src/components/features/sidebar/sidebar.tsx
-
curl -L -o sidebar.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/sidebar/sidebar.tsx
10.5 kB
| import React from "react"; | |
| import { useTranslation } from "react-i18next"; | |
| import { SidebarRailBody } from "./sidebar-rail-body"; | |
| import { getErrorStatus, useSettings } from "#/hooks/query/use-settings"; | |
| import { useConfig } from "#/hooks/query/use-config"; | |
| import { displayErrorToast } from "#/utils/custom-toast-handlers"; | |
| import { I18nKey } from "#/i18n/declaration"; | |
| import { useNavigation } from "#/context/navigation-context"; | |
| import { useActiveBackendContext } from "#/contexts/active-backend-context"; | |
| import { cn } from "#/utils/utils"; | |
| import { useSidebarMobileNav } from "./sidebar-mobile-nav-context"; | |
| import { useSidebarStore } from "#/stores/sidebar-store"; | |
| import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; | |
| import { useBackendsHealth } from "#/hooks/query/use-backends-health"; | |
| // The LLM settings modal is only mounted when the settings query 404s and | |
| // LLM settings aren't hidden — keep it out of the sidebar's eager graph. | |
| const SettingsModal = React.lazy(() => | |
| import("#/components/shared/modals/settings/settings-modal").then((m) => ({ | |
| default: m.SettingsModal, | |
| })), | |
| ); | |
| // Add/Manage backend modals are lifted into the sidebar (instead of living | |
| // inside BackendSelector) so they survive the collapsed popover unmounting | |
| // when the user moves the cursor out of the popover toward the modal. | |
| const AddBackendModal = React.lazy(() => | |
| import("#/components/features/backends/add-backend-modal").then((m) => ({ | |
| default: m.AddBackendModal, | |
| })), | |
| ); | |
| const ManageBackendsModal = React.lazy(() => | |
| import("#/components/features/backends/manage-backends-modal").then((m) => ({ | |
| default: m.ManageBackendsModal, | |
| })), | |
| ); | |
| const MOBILE_DRAWER_TRANSITION_MS = 250; | |
| export function Sidebar() { | |
| const { t } = useTranslation("openhands"); | |
| const { currentPath } = useNavigation(); | |
| const { data: config } = useConfig(); | |
| const { | |
| data: settings, | |
| error: settingsError, | |
| isError: settingsIsError, | |
| isFetching: isFetchingSettings, | |
| } = useSettings(); | |
| const { backends, active } = useActiveBackendContext(); | |
| const healthByBackendId = useBackendsHealth(backends); | |
| const activeBackendHealth = healthByBackendId[active.backend.id]; | |
| const collapsed = useSidebarStore((state) => state.collapsed); | |
| const setCollapsed = useSidebarStore((state) => state.setCollapsed); | |
| const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false); | |
| const [collapsedBackendPopoverOpen, setCollapsedBackendPopoverOpen] = | |
| React.useState(false); | |
| const collapsedBackendCloseTimer = React.useRef<ReturnType< | |
| typeof setTimeout | |
| > | null>(null); | |
| // Lifted out of BackendSelector so opening these modals from the | |
| // collapsed-sidebar popover doesn't lose state when the popover unmounts | |
| // (cursor moving toward the modal triggers onMouseLeave -> close). | |
| const [addBackendModalOpen, setAddBackendModalOpen] = React.useState(false); | |
| const [manageBackendsModalOpen, setManageBackendsModalOpen] = | |
| React.useState(false); | |
| const [collapsedRailHovered, setCollapsedRailHovered] = React.useState(false); | |
| const suppressCollapsedExpandRef = React.useRef(false); | |
| const [, refreshCollapsedExpandGate] = React.useReducer((n) => n + 1, 0); | |
| const { isOpen: isMobileNavOpen, close: closeMobileNav } = | |
| useSidebarMobileNav(); | |
| const [mobileDrawerMounted, setMobileDrawerMounted] = React.useState(false); | |
| const [mobileDrawerVisible, setMobileDrawerVisible] = React.useState(false); | |
| const collapsedBackendPopoverRef = useClickOutsideElement<HTMLDivElement>( | |
| () => setCollapsedBackendPopoverOpen(false), | |
| ); | |
| const settingsErrorStatus = getErrorStatus(settingsError); | |
| React.useEffect(() => { | |
| closeMobileNav(); | |
| }, [currentPath, closeMobileNav]); | |
| React.useEffect(() => { | |
| if (isMobileNavOpen) { | |
| setMobileDrawerMounted(true); | |
| const frame = requestAnimationFrame(() => { | |
| setMobileDrawerVisible(true); | |
| }); | |
| return () => cancelAnimationFrame(frame); | |
| } | |
| setMobileDrawerVisible(false); | |
| const timer = window.setTimeout(() => { | |
| setMobileDrawerMounted(false); | |
| }, MOBILE_DRAWER_TRANSITION_MS); | |
| return () => window.clearTimeout(timer); | |
| }, [isMobileNavOpen]); | |
| React.useEffect(() => { | |
| if (!isMobileNavOpen) { | |
| return undefined; | |
| } | |
| const onKeyDown = (event: KeyboardEvent) => { | |
| if (event.key === "Escape") { | |
| closeMobileNav(); | |
| } | |
| }; | |
| window.addEventListener("keydown", onKeyDown); | |
| return () => window.removeEventListener("keydown", onKeyDown); | |
| }, [isMobileNavOpen, closeMobileNav]); | |
| React.useEffect(() => { | |
| if (currentPath === "/settings") { | |
| setSettingsModalIsOpen(false); | |
| } else if ( | |
| !isFetchingSettings && | |
| settingsIsError && | |
| settingsErrorStatus !== 404 | |
| ) { | |
| // We don't show toast errors for settings in the global error handler | |
| // because we have a special case for 404 errors | |
| displayErrorToast(t(I18nKey.SETTINGS$FETCH_ERROR)); | |
| } else if ( | |
| settingsErrorStatus === 404 && | |
| !config?.feature_flags?.hide_llm_settings | |
| ) { | |
| setSettingsModalIsOpen(true); | |
| } | |
| }, [ | |
| currentPath, | |
| isFetchingSettings, | |
| settingsIsError, | |
| settingsErrorStatus, | |
| config?.feature_flags?.hide_llm_settings, | |
| t, | |
| ]); | |
| const collapseToggleLabel = t( | |
| collapsed ? I18nKey.SIDEBAR$EXPAND : I18nKey.SIDEBAR$COLLAPSE, | |
| ); | |
| const handleCollapsedRailClick = React.useCallback( | |
| (event: React.MouseEvent<HTMLElement>) => { | |
| if (!collapsed) { | |
| return; | |
| } | |
| const target = event.target; | |
| if (!(target instanceof HTMLElement)) { | |
| return; | |
| } | |
| // Keep existing behavior for explicit controls/links and only use | |
| // this as a convenience hit-area for empty collapsed-rail space. | |
| if ( | |
| target.closest( | |
| "a,button,input,textarea,select,[role='button'],[role='link']", | |
| ) | |
| ) { | |
| return; | |
| } | |
| setCollapsed(false); | |
| }, | |
| [collapsed, setCollapsed], | |
| ); | |
| const handleCollapse = React.useCallback(() => { | |
| setCollapsedRailHovered(false); | |
| suppressCollapsedExpandRef.current = true; | |
| refreshCollapsedExpandGate(); | |
| setCollapsed(true); | |
| window.setTimeout(() => { | |
| suppressCollapsedExpandRef.current = false; | |
| refreshCollapsedExpandGate(); | |
| }, 250); | |
| }, [setCollapsed]); | |
| const showCollapsedExpandButton = | |
| collapsed && collapsedRailHovered && !suppressCollapsedExpandRef.current; | |
| const isExtensionsActive = | |
| currentPath === "/customize" || | |
| currentPath.startsWith("/skills") || | |
| currentPath === "/plugins" || | |
| currentPath === "/apps" || | |
| currentPath === "/mcp"; | |
| const railBodyProps = { | |
| collapseToggleLabel, | |
| onCollapse: handleCollapse, | |
| onExpand: () => setCollapsed(false), | |
| showCollapsedExpandButton, | |
| isExtensionsActive, | |
| currentPath, | |
| activeBackend: active.backend, | |
| activeOrgId: active.orgId, | |
| activeBackendHealth, | |
| collapsedBackendPopoverOpen, | |
| setCollapsedBackendPopoverOpen, | |
| collapsedBackendPopoverRef, | |
| collapsedBackendCloseTimer, | |
| onOpenAddBackend: () => setAddBackendModalOpen(true), | |
| onOpenManageBackends: () => setManageBackendsModalOpen(true), | |
| }; | |
| return ( | |
| <> | |
| {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the aside acts as a hit-area for the collapsed rail; nested controls handle their own keyboard interactions. */} | |
| <aside | |
| aria-label={t(I18nKey.SIDEBAR$NAVIGATION_LABEL)} | |
| data-collapsed={collapsed ? "true" : "false"} | |
| onClick={handleCollapsedRailClick} | |
| onMouseEnter={() => { | |
| if (collapsed) { | |
| setCollapsedRailHovered(true); | |
| } | |
| }} | |
| onMouseLeave={() => { | |
| setCollapsedRailHovered(false); | |
| }} | |
| className={cn( | |
| "max-md:hidden flex bg-base flex-col min-h-0 transition-[width,min-width] duration-200", | |
| "md:border-r md:border-[var(--oh-border)] md:h-full", | |
| collapsed | |
| ? "md:w-[60px] md:min-w-[60px] md:px-2.5" | |
| : "md:w-[300px] md:min-w-[300px] pb-2 md:pl-2.5 md:pr-0", | |
| currentPath === "/" && "md:pb-3", | |
| )} | |
| > | |
| <SidebarRailBody | |
| collapsed={collapsed} | |
| showCollapseToggle | |
| {...railBodyProps} | |
| /> | |
| </aside> | |
| {mobileDrawerMounted ? ( | |
| <> | |
| <div | |
| className={cn( | |
| "fixed inset-0 z-40 bg-black/50 md:hidden", | |
| "transition-opacity ease-in-out motion-reduce:transition-none", | |
| mobileDrawerVisible | |
| ? "opacity-100" | |
| : "pointer-events-none opacity-0", | |
| )} | |
| style={{ transitionDuration: `${MOBILE_DRAWER_TRANSITION_MS}ms` }} | |
| onClick={closeMobileNav} | |
| aria-hidden={!mobileDrawerVisible} | |
| /> | |
| <aside | |
| aria-label={t(I18nKey.SIDEBAR$NAVIGATION_LABEL)} | |
| data-testid="sidebar-mobile-drawer" | |
| aria-hidden={!mobileDrawerVisible} | |
| className={cn( | |
| "fixed inset-y-0 left-0 z-50 flex min-h-0 w-[min(300px,85vw)] flex-col bg-base", | |
| "border-r border-[var(--oh-border)] pb-2 pl-2.5 pr-0 md:hidden", | |
| "transition-transform ease-in-out motion-reduce:transition-none", | |
| mobileDrawerVisible ? "translate-x-0" : "-translate-x-full", | |
| )} | |
| style={{ transitionDuration: `${MOBILE_DRAWER_TRANSITION_MS}ms` }} | |
| > | |
| <SidebarRailBody | |
| collapsed={false} | |
| showCollapseToggle={false} | |
| showMobileCloseButton | |
| onCloseMobile={closeMobileNav} | |
| {...railBodyProps} | |
| /> | |
| </aside> | |
| </> | |
| ) : null} | |
| {settingsModalIsOpen && ( | |
| <React.Suspense fallback={null}> | |
| <SettingsModal | |
| settings={settings} | |
| onClose={() => setSettingsModalIsOpen(false)} | |
| /> | |
| </React.Suspense> | |
| )} | |
| {addBackendModalOpen && ( | |
| <React.Suspense fallback={null}> | |
| <AddBackendModal onClose={() => setAddBackendModalOpen(false)} /> | |
| </React.Suspense> | |
| )} | |
| {manageBackendsModalOpen && ( | |
| <React.Suspense fallback={null}> | |
| <ManageBackendsModal | |
| onClose={() => setManageBackendsModalOpen(false)} | |
| /> | |
| </React.Suspense> | |
| )} | |
| </> | |
| ); | |
| } | |