import React, { useMemo, useEffect, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { Typography } from "#/ui/typography"; import { I18nKey } from "#/i18n/declaration"; import { ComboboxCaretInline } from "#/ui/combobox-caret"; import LessonPlanIcon from "#/icons/lesson-plan.svg?react"; import { CodePillIcon } from "#/icons/code-pill"; import { useConversationStore } from "#/stores/conversation-store"; import { ChangeAgentContextMenu } from "./change-agent-context-menu"; import { cn } from "#/utils/utils"; import { formControlMutedHoverClassName, formControlTransitionClassName, } from "#/utils/form-control-classes"; import { useAgentState } from "#/hooks/use-agent-state"; import { AgentState } from "#/types/agent-state"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { useUnifiedWebSocketStatus } from "#/hooks/use-unified-websocket-status"; import { useSubConversationTaskPolling } from "#/hooks/query/use-sub-conversation-task-polling"; import { useHandlePlanClick } from "#/hooks/use-handle-plan-click"; import { useOptionalConversationId } from "#/hooks/use-conversation-id"; import { StyledTooltip } from "#/components/shared/buttons/styled-tooltip"; export function ChangeAgentButton() { const [contextMenuOpen, setContextMenuOpen] = useState(false); const { conversationMode, setConversationMode, subConversationTaskId } = useConversationStore(); const { conversationId } = useOptionalConversationId(); const isHomePage = !conversationId; const webSocketStatus = useUnifiedWebSocketStatus(); const isWebSocketConnected = webSocketStatus === "OPEN"; const { curAgentState } = useAgentState(); const { t } = useTranslation("openhands"); const isAgentRunning = curAgentState === AgentState.RUNNING; const { data: conversation } = useActiveConversation(); const queryClient = useQueryClient(); // Track the last invalidated task ID to prevent duplicate invalidations const lastInvalidatedTaskIdRef = useRef(null); // Poll sub-conversation task status const { taskStatus, subConversationId } = useSubConversationTaskPolling( subConversationTaskId, conversation?.id || null, ); // Invalidate parent conversation cache when task is ready (only once per task) useEffect(() => { if ( taskStatus === "READY" && subConversationId && conversation?.id && subConversationTaskId && lastInvalidatedTaskIdRef.current !== subConversationTaskId ) { // Mark this task as invalidated to prevent duplicate calls lastInvalidatedTaskIdRef.current = subConversationTaskId; // Invalidate the parent conversation to refetch with updated sub_conversation_ids queryClient.invalidateQueries({ queryKey: ["user", "conversation", conversation.id], }); } }, [ taskStatus, subConversationId, conversation?.id, subConversationTaskId, queryClient, ]); // Get handlePlanClick and isCreatingConversation from custom hook const { handlePlanClick, isCreatingConversation } = useHandlePlanClick(); // Close context menu when agent starts running useEffect(() => { if ((isAgentRunning || !isWebSocketConnected) && contextMenuOpen) { setContextMenuOpen(false); } }, [isAgentRunning, contextMenuOpen, isWebSocketConnected]); const isButtonDisabled = isHomePage || isAgentRunning || isCreatingConversation || !isWebSocketConnected; // Handle Shift + Tab keyboard shortcut to cycle through modes useEffect(() => { if (isButtonDisabled) { return undefined; } const handleKeyDown = (event: KeyboardEvent) => { // Check for Shift + Tab combination if (event.shiftKey && event.key === "Tab") { // Prevent default tab navigation behavior event.preventDefault(); event.stopPropagation(); // Cycle between modes: code -> plan -> code const nextMode = conversationMode === "code" ? "plan" : "code"; if (nextMode === "plan") { handlePlanClick(event); } else { setConversationMode(nextMode); } } }; document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); }; }, [ isButtonDisabled, conversationMode, setConversationMode, handlePlanClick, ]); const handleButtonClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); setContextMenuOpen(!contextMenuOpen); }; const handleCodeClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); setConversationMode("code"); }; const isExecutionAgent = conversationMode === "code"; const buttonLabel = useMemo(() => { if (isExecutionAgent) { return t(I18nKey.COMMON$CODE); } return t(I18nKey.COMMON$PLAN); }, [isExecutionAgent, t]); const buttonIcon = useMemo(() => { if (isExecutionAgent) { return ; } return ; }, [isExecutionAgent]); const button = (
{contextMenuOpen && ( setContextMenuOpen(false)} onCodeClick={handleCodeClick} onPlanClick={handlePlanClick} /> )}
); if (isHomePage) { return ( {button} ); } return button; }