Download src/components/features/skills/extensions-navigation.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 5.3 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/skills/extensions-navigation.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/src/components/features/skills/extensions-navigation.tsx
-
curl -L -o extensions-navigation.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/skills/extensions-navigation.tsx
5.3 kB
| import { useTranslation } from "react-i18next"; | |
| import { Blocks, ExternalLink } from "lucide-react"; | |
| import { NavigationLink } from "#/components/shared/navigation-link"; | |
| import { cn } from "#/utils/utils"; | |
| import SkillsIcon from "#/icons/skills.svg?react"; | |
| import ServerProcessIcon from "#/icons/server-process.svg?react"; | |
| import { BackendSyncedSettingsBadge } from "#/components/features/settings/backend-synced-settings-badge"; | |
| import { | |
| SIDEBAR_ROW_INTERACTIVE_CLASS, | |
| sidebarNavRowClassName, | |
| } from "#/components/features/sidebar/sidebar-layout"; | |
| import { I18nKey } from "#/i18n/declaration"; | |
| import { useActiveBackendContext } from "#/contexts/active-backend-context"; | |
| import { isNoBackend } from "#/api/backend-registry/active-store"; | |
| /** Only the Skills item points to a cloud-hosted page today. */ | |
| const CLOUD_LINKED_EXTENSION_PATH = "/skills"; | |
| /** Backend-installed artifacts are not available on Cloud backends yet. */ | |
| const CLOUD_HIDDEN_EXTENSION_PATHS = new Set(["/plugins", "/apps"]); | |
| interface ExtensionNavItem { | |
| to: string; | |
| label: string; | |
| icon: React.ReactElement; | |
| end?: boolean; | |
| comingSoon?: boolean; | |
| } | |
| export const EXTENSIONS_NAV_ITEMS: ExtensionNavItem[] = [ | |
| { | |
| to: "/mcp", | |
| label: "MCP Servers", | |
| icon: <ServerProcessIcon width={16} height={16} />, | |
| end: true, | |
| }, | |
| { | |
| to: "/skills", | |
| label: "Skills", | |
| icon: <SkillsIcon width={16} height={16} aria-hidden="true" />, | |
| end: true, | |
| }, | |
| { | |
| to: "/plugins", | |
| label: "Plugins", | |
| icon: ( | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| stroke="currentColor" | |
| strokeWidth="2" | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| width={16} | |
| height={16} | |
| aria-hidden="true" | |
| > | |
| <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" /> | |
| <path d="m3.3 7 8.7 5 8.7-5" /> | |
| <path d="M12 22V12" /> | |
| </svg> | |
| ), | |
| end: true, | |
| }, | |
| { | |
| to: "/apps", | |
| label: "Apps", | |
| icon: <Blocks width={16} height={16} aria-hidden="true" />, | |
| end: true, | |
| }, | |
| ]; | |
| export function ExtensionsNavigation() { | |
| const { t } = useTranslation("openhands"); | |
| const { active } = useActiveBackendContext(); | |
| const { backend } = active; | |
| const isCloudBackend = !isNoBackend(backend) && backend.kind === "cloud"; | |
| return ( | |
| <aside | |
| data-testid="extensions-navbar-desktop" | |
| className="hidden md:flex md:w-[260px] md:shrink-0 md:flex-col md:gap-2 md:sticky md:top-8 md:self-start" | |
| > | |
| <span className="px-2 text-sm font-normal text-white"> | |
| {t(I18nKey.NAV$CUSTOMIZE)} | |
| </span> | |
| <div className="flex flex-col gap-0.5 pt-0.5"> | |
| {EXTENSIONS_NAV_ITEMS.filter( | |
| (item) => | |
| !(CLOUD_HIDDEN_EXTENSION_PATHS.has(item.to) && isCloudBackend), | |
| ).map((item) => { | |
| const isCloudSkillsLink = | |
| item.to === CLOUD_LINKED_EXTENSION_PATH && isCloudBackend; | |
| const baseRow = ( | |
| <span className="shrink-0 flex items-center justify-center"> | |
| {item.icon} | |
| </span> | |
| ); | |
| const label = ( | |
| <span className="truncate"> | |
| {isCloudSkillsLink | |
| ? t(I18nKey.SIDEBAR$SKILLS_AND_PLUGINS_CLOUD_LINK) | |
| : item.label} | |
| </span> | |
| ); | |
| const comingSoonBadge = item.comingSoon && ( | |
| <span className="ml-auto shrink-0 rounded-full border border-white/20 bg-white/5 px-1.5 py-0.5 text-[10px] font-medium text-[var(--oh-text-dim)]"> | |
| {t(I18nKey.NAV$COMING_SOON)} | |
| </span> | |
| ); | |
| if (isCloudSkillsLink) { | |
| const cloudSkillsUrl = `${backend.host.replace(/\/+$/, "")}/settings/skills`; | |
| return ( | |
| <a | |
| key={item.to} | |
| data-testid={`sidebar-extensions-${item.to}`} | |
| href={cloudSkillsUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className={cn( | |
| sidebarNavRowClassName(), | |
| "truncate", | |
| SIDEBAR_ROW_INTERACTIVE_CLASS.idle, | |
| )} | |
| > | |
| {baseRow} | |
| {label} | |
| <ExternalLink | |
| className="ml-auto size-4 shrink-0 text-[var(--oh-muted)]" | |
| aria-hidden | |
| /> | |
| </a> | |
| ); | |
| } | |
| return ( | |
| <NavigationLink | |
| key={item.to} | |
| to={item.to} | |
| end={item.end} | |
| data-testid={`sidebar-extensions-${item.to}`} | |
| className={({ isActive }) => | |
| cn( | |
| sidebarNavRowClassName(), | |
| "truncate", | |
| isActive | |
| ? SIDEBAR_ROW_INTERACTIVE_CLASS.active | |
| : SIDEBAR_ROW_INTERACTIVE_CLASS.idle, | |
| ) | |
| } | |
| > | |
| {baseRow} | |
| {label} | |
| {comingSoonBadge} | |
| </NavigationLink> | |
| ); | |
| })} | |
| </div> | |
| <div className="px-2 pt-3"> | |
| <BackendSyncedSettingsBadge /> | |
| </div> | |
| </aside> | |
| ); | |
| } | |