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) => void; onShowPlugins: (event: React.MouseEvent) => void; onShowHooks: (event: React.MouseEvent) => void; onShowAgentTools: (event: React.MouseEvent) => 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(handleClose); return ( {/* 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 && (
handleSubmenuClick("agent-profile")} > } text={t(I18nKey.CHAT$SWITCH_AGENT_PROFILE)} rightIcon={} />
{/* 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. */}
)} {/* Git Tools */} {showGitTools && (
handleSubmenuClick("git")} isDisabled={isArchivedConversation} > } text={t(I18nKey.COMMON$GIT_TOOLS)} rightIcon={} /> {!isArchivedConversation && (
)}
)} {/* Macros */}
handleSubmenuClick("macros")} isDisabled={isArchivedConversation} > } text={t(I18nKey.COMMON$MACROS)} rightIcon={} /> {!isArchivedConversation && (
)}
{shouldShowAgentTools && } } text={t(I18nKey.CONVERSATION$SHOW_SKILLS)} /> {/* Show Plugins - only when this conversation has attached plugins */} {shouldShowPlugins && ( } text={t(I18nKey.CONVERSATION$SHOW_PLUGINS)} /> )} {/* Show Hooks - Only show for V1 conversations */} {shouldShowHooks && ( } text={t(I18nKey.CONVERSATION$SHOW_HOOKS)} /> )} {/* Show Agent Tools and Metadata - Only show if system message is available */} {shouldShowAgentTools && ( } text={t(I18nKey.BUTTON$SHOW_AGENT_TOOLS_AND_METADATA)} /> )} {footerAction && ( <> { event.preventDefault(); event.stopPropagation(); footerAction.onClick(); handleClose(); }} > )}
); }