Download src/components/features/controls/tools-context-menu.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/controls/tools-context-menu.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/src/components/features/controls/tools-context-menu.tsx
-
curl -L -o tools-context-menu.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/controls/tools-context-menu.tsx
11.3 kB
| import React, { useState } from "react"; | |
| import { useTranslation } from "react-i18next"; | |
| import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; | |
| import { useActiveConversation } from "#/hooks/query/use-active-conversation"; | |
| import { useUserProviders } from "#/hooks/use-user-providers"; | |
| import { cn } from "#/utils/utils"; | |
| import { ContextMenu } from "#/ui/context-menu"; | |
| import { ContextMenuListItem } from "../context-menu/context-menu-list-item"; | |
| import { Divider } from "#/ui/divider"; | |
| import { I18nKey } from "#/i18n/declaration"; | |
| import CodeBranchIcon from "#/icons/u-code-branch.svg?react"; | |
| import SkillsIcon from "#/icons/skills.svg?react"; | |
| import PuzzleIcon from "#/icons/u-puzzle-piece.svg?react"; | |
| import FishingHookIcon from "#/icons/fishing-hook.svg?react"; | |
| import ToolsIcon from "#/icons/u-tools.svg?react"; | |
| import RobotIcon from "#/icons/u-robot.svg?react"; | |
| import SettingsIcon from "#/icons/settings.svg?react"; | |
| import CarretRightFillIcon from "#/icons/carret-right-fill.svg?react"; | |
| import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text"; | |
| import { GitToolsSubmenu } from "./git-tools-submenu"; | |
| import { MacrosSubmenu } from "./macros-submenu"; | |
| import { ChatInputProfileMenuContent } from "#/components/features/chat/components/chat-input-profile-picker"; | |
| import { ArchivedDisabledTooltip } from "../context-menu/archived-disabled-tooltip"; | |
| import { useIsArchivedConversation } from "#/hooks/use-is-archived-conversation"; | |
| interface ToolsContextMenuProps { | |
| onClose: () => void; | |
| onShowSkills: (event: React.MouseEvent<HTMLButtonElement>) => void; | |
| onShowPlugins: (event: React.MouseEvent<HTMLButtonElement>) => void; | |
| onShowHooks: (event: React.MouseEvent<HTMLButtonElement>) => void; | |
| onShowAgentTools: (event: React.MouseEvent<HTMLButtonElement>) => void; | |
| shouldShowAgentTools?: boolean; | |
| shouldShowHooks?: boolean; | |
| shouldShowPlugins?: boolean; | |
| /** | |
| * Offer the "Switch agent profile" submenu (OSS-5735). The caller owns the | |
| * gating (pre-start only + profiles available) so this menu stays renderable | |
| * without query/navigation providers when the item is off. | |
| */ | |
| showAgentProfileSwitch?: boolean; | |
| /** When set, renders a divider and this action as the last menu item. */ | |
| footerAction?: { | |
| testId: string; | |
| icon: React.ReactNode; | |
| label: string; | |
| onClick: () => void; | |
| }; | |
| } | |
| export function ToolsContextMenu({ | |
| onClose, | |
| onShowSkills, | |
| onShowPlugins, | |
| onShowHooks, | |
| onShowAgentTools, | |
| shouldShowAgentTools = true, | |
| shouldShowHooks = false, | |
| shouldShowPlugins = false, | |
| showAgentProfileSwitch = false, | |
| footerAction, | |
| }: ToolsContextMenuProps) { | |
| const { t } = useTranslation("openhands"); | |
| const { data: conversation } = useActiveConversation(); | |
| const { providers } = useUserProviders(); | |
| const isArchivedConversation = useIsArchivedConversation(); | |
| const [activeSubmenu, setActiveSubmenu] = useState< | |
| "git" | "macros" | "agent-profile" | null | |
| >(null); | |
| const hasRepository = !!conversation?.selected_repository; | |
| const providersAreSet = providers.length > 0; | |
| const showGitTools = hasRepository && providersAreSet; | |
| const handleSubmenuClick = (submenu: "git" | "macros" | "agent-profile") => { | |
| if (isArchivedConversation) { | |
| return; | |
| } | |
| setActiveSubmenu(activeSubmenu === submenu ? null : submenu); | |
| }; | |
| const handleClose = () => { | |
| setActiveSubmenu(null); | |
| onClose(); | |
| }; | |
| const ref = useClickOutsideElement<HTMLUListElement>(handleClose); | |
| return ( | |
| <ContextMenu | |
| ref={ref} | |
| testId="tools-context-menu" | |
| position="top" | |
| alignment="left" | |
| className="left-[-16px] mb-2 bottom-full overflow-visible min-w-[200px]" | |
| > | |
| {/* Switch agent profile — only while starting a new conversation; the | |
| profile is locked once the conversation starts (OSS-5735). Selecting | |
| a profile activates it (home) or recreates the blank conversation | |
| with it (see ChatInputProfileMenuContent). No archived gating: this | |
| never renders in a started (archivable) conversation. */} | |
| {showAgentProfileSwitch && ( | |
| <div className="relative group/agent-profile"> | |
| <ContextMenuListItem | |
| testId="switch-agent-profile-button" | |
| onClick={() => handleSubmenuClick("agent-profile")} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={<RobotIcon width={16} height={16} aria-hidden />} | |
| text={t(I18nKey.CHAT$SWITCH_AGENT_PROFILE)} | |
| rightIcon={<CarretRightFillIcon width={10} height={10} />} | |
| /> | |
| </ContextMenuListItem> | |
| <div | |
| className={cn( | |
| "absolute left-full top-[-4px] z-60 opacity-0 invisible pointer-events-none transition-all duration-200 ml-[1px]", | |
| "group-hover/agent-profile:opacity-100 group-hover/agent-profile:visible group-hover/agent-profile:pointer-events-auto", | |
| "hover:opacity-100 hover:visible hover:pointer-events-auto", | |
| activeSubmenu === "agent-profile" && | |
| "opacity-100 visible pointer-events-auto", | |
| )} | |
| > | |
| {/* overflow-y-auto so a long profile list scrolls within the menu; | |
| safe because the content has no floating children — only the | |
| flat profile list + Manage link. */} | |
| <ContextMenu | |
| testId="agent-profile-submenu" | |
| className="min-w-[220px] max-w-[320px] max-h-[60vh] overflow-y-auto gap-0" | |
| > | |
| <ChatInputProfileMenuContent | |
| onClose={handleClose} | |
| dividerInset="menu" | |
| /> | |
| </ContextMenu> | |
| </div> | |
| </div> | |
| )} | |
| {/* Git Tools */} | |
| {showGitTools && ( | |
| <div className="relative group/git"> | |
| <ArchivedDisabledTooltip isDisabled={isArchivedConversation}> | |
| <ContextMenuListItem | |
| testId="git-tools-button" | |
| onClick={() => handleSubmenuClick("git")} | |
| isDisabled={isArchivedConversation} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={<CodeBranchIcon width={16} height={16} />} | |
| text={t(I18nKey.COMMON$GIT_TOOLS)} | |
| rightIcon={<CarretRightFillIcon width={10} height={10} />} | |
| /> | |
| </ContextMenuListItem> | |
| </ArchivedDisabledTooltip> | |
| {!isArchivedConversation && ( | |
| <div | |
| className={cn( | |
| "absolute left-full top-[-6px] z-60 opacity-0 invisible pointer-events-none transition-all duration-200 ml-[1px]", | |
| "group-hover/git:opacity-100 group-hover/git:visible group-hover/git:pointer-events-auto", | |
| "hover:opacity-100 hover:visible hover:pointer-events-auto", | |
| activeSubmenu === "git" && | |
| "opacity-100 visible pointer-events-auto", | |
| )} | |
| > | |
| <GitToolsSubmenu onClose={handleClose} /> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Macros */} | |
| <div className="relative group/macros"> | |
| <ArchivedDisabledTooltip isDisabled={isArchivedConversation}> | |
| <ContextMenuListItem | |
| testId="macros-button" | |
| onClick={() => handleSubmenuClick("macros")} | |
| isDisabled={isArchivedConversation} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={<SettingsIcon width={16} height={16} />} | |
| text={t(I18nKey.COMMON$MACROS)} | |
| rightIcon={<CarretRightFillIcon width={10} height={10} />} | |
| /> | |
| </ContextMenuListItem> | |
| </ArchivedDisabledTooltip> | |
| {!isArchivedConversation && ( | |
| <div | |
| className={cn( | |
| "absolute left-full top-[-4px] z-60 opacity-0 invisible pointer-events-none transition-all duration-200 ml-[1px]", | |
| "group-hover/macros:opacity-100 group-hover/macros:visible group-hover/macros:pointer-events-auto", | |
| "hover:opacity-100 hover:visible hover:pointer-events-auto", | |
| activeSubmenu === "macros" && | |
| "opacity-100 visible pointer-events-auto", | |
| )} | |
| > | |
| <MacrosSubmenu onClose={handleClose} /> | |
| </div> | |
| )} | |
| </div> | |
| {shouldShowAgentTools && <Divider inset="menu" />} | |
| <ArchivedDisabledTooltip isDisabled={isArchivedConversation}> | |
| <ContextMenuListItem | |
| testId="show-skills-button" | |
| onClick={onShowSkills} | |
| isDisabled={isArchivedConversation} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={ | |
| <SkillsIcon | |
| width={16} | |
| height={16} | |
| className="stroke-[1.75]" | |
| aria-hidden | |
| /> | |
| } | |
| text={t(I18nKey.CONVERSATION$SHOW_SKILLS)} | |
| /> | |
| </ContextMenuListItem> | |
| </ArchivedDisabledTooltip> | |
| {/* Show Plugins - only when this conversation has attached plugins */} | |
| {shouldShowPlugins && ( | |
| <ArchivedDisabledTooltip isDisabled={isArchivedConversation}> | |
| <ContextMenuListItem | |
| testId="show-plugins-button" | |
| onClick={onShowPlugins} | |
| isDisabled={isArchivedConversation} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={<PuzzleIcon width={16} height={16} aria-hidden />} | |
| text={t(I18nKey.CONVERSATION$SHOW_PLUGINS)} | |
| /> | |
| </ContextMenuListItem> | |
| </ArchivedDisabledTooltip> | |
| )} | |
| {/* Show Hooks - Only show for V1 conversations */} | |
| {shouldShowHooks && ( | |
| <ArchivedDisabledTooltip isDisabled={isArchivedConversation}> | |
| <ContextMenuListItem | |
| testId="show-hooks-button" | |
| onClick={onShowHooks} | |
| isDisabled={isArchivedConversation} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={<FishingHookIcon width={16} height={16} aria-hidden />} | |
| text={t(I18nKey.CONVERSATION$SHOW_HOOKS)} | |
| /> | |
| </ContextMenuListItem> | |
| </ArchivedDisabledTooltip> | |
| )} | |
| {/* Show Agent Tools and Metadata - Only show if system message is available */} | |
| {shouldShowAgentTools && ( | |
| <ArchivedDisabledTooltip isDisabled={isArchivedConversation}> | |
| <ContextMenuListItem | |
| testId="show-agent-tools-button" | |
| onClick={onShowAgentTools} | |
| isDisabled={isArchivedConversation} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={<ToolsIcon width={16} height={16} />} | |
| text={t(I18nKey.BUTTON$SHOW_AGENT_TOOLS_AND_METADATA)} | |
| /> | |
| </ContextMenuListItem> | |
| </ArchivedDisabledTooltip> | |
| )} | |
| {footerAction && ( | |
| <> | |
| <Divider /> | |
| <ContextMenuListItem | |
| testId={footerAction.testId} | |
| onClick={(event) => { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| footerAction.onClick(); | |
| handleClose(); | |
| }} | |
| > | |
| <ToolsContextMenuIconText | |
| icon={footerAction.icon} | |
| text={footerAction.label} | |
| /> | |
| </ContextMenuListItem> | |
| </> | |
| )} | |
| </ContextMenu> | |
| ); | |
| } | |