From d6fab190bfb55f07cc380993b45ec4f781706a92 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Sat, 15 Nov 2025 09:43:21 +0700 Subject: [PATCH] feat(frontend): integrate with the API to create a sub-conversation for the planning agent (#11730) --- .../v1-conversation-service.api.ts | 4 ++ .../v1-conversation-service.types.ts | 2 + frontend/src/api/open-hands.types.ts | 1 + .../features/chat/change-agent-button.tsx | 66 +++++++++++++++---- .../hooks/mutation/use-create-conversation.ts | 6 ++ frontend/src/i18n/declaration.ts | 1 + frontend/src/i18n/translation.json | 16 +++++ 7 files changed, 84 insertions(+), 12 deletions(-) diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts index 93cd2ba85e..5ca7daf09a 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts @@ -60,6 +60,8 @@ class V1ConversationService { selected_branch?: string, conversationInstructions?: string, trigger?: ConversationTrigger, + parent_conversation_id?: string, + agent_type?: "default" | "plan", ): Promise { const body: V1AppConversationStartRequest = { selected_repository: selectedRepository, @@ -67,6 +69,8 @@ class V1ConversationService { selected_branch, title: conversationInstructions, trigger, + parent_conversation_id: parent_conversation_id || null, + agent_type, }; // Add initial message if provided diff --git a/frontend/src/api/conversation-service/v1-conversation-service.types.ts b/frontend/src/api/conversation-service/v1-conversation-service.types.ts index b48ce5bd6b..3441448472 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.types.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.types.ts @@ -30,6 +30,8 @@ export interface V1AppConversationStartRequest { title?: string | null; trigger?: ConversationTrigger | null; pr_number?: number[]; + parent_conversation_id?: string | null; + agent_type?: "default" | "plan"; } export type V1AppConversationStartTaskStatus = diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index 9a30e46027..47d34fe567 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -77,6 +77,7 @@ export interface Conversation { session_api_key: string | null; pr_number?: number[] | null; conversation_version?: "V0" | "V1"; + sub_conversation_ids?: string[]; } export interface ResultSet { diff --git a/frontend/src/components/features/chat/change-agent-button.tsx b/frontend/src/components/features/chat/change-agent-button.tsx index cf27d96554..6d41f5cfc1 100644 --- a/frontend/src/components/features/chat/change-agent-button.tsx +++ b/frontend/src/components/features/chat/change-agent-button.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useEffect } from "react"; +import React, { useMemo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Typography } from "#/ui/typography"; import { I18nKey } from "#/i18n/declaration"; @@ -11,10 +11,12 @@ import { cn } from "#/utils/utils"; import { USE_PLANNING_AGENT } from "#/utils/feature-flags"; import { useAgentState } from "#/hooks/use-agent-state"; import { AgentState } from "#/types/agent-state"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; +import { useCreateConversation } from "#/hooks/mutation/use-create-conversation"; +import { displaySuccessToast } from "#/utils/custom-toast-handlers"; export function ChangeAgentButton() { - const { t } = useTranslation(); - const [contextMenuOpen, setContextMenuOpen] = React.useState(false); + const [contextMenuOpen, setContextMenuOpen] = useState(false); const conversationMode = useConversationStore( (state) => state.conversationMode, @@ -28,8 +30,14 @@ export function ChangeAgentButton() { const { curAgentState } = useAgentState(); + const { t } = useTranslation(); + const isAgentRunning = curAgentState === AgentState.RUNNING; + const { data: conversation } = useActiveConversation(); + const { mutate: createConversation, isPending: isCreatingConversation } = + useCreateConversation(); + // Close context menu when agent starts running useEffect(() => { if (isAgentRunning && contextMenuOpen) { @@ -37,6 +45,40 @@ export function ChangeAgentButton() { } }, [isAgentRunning, contextMenuOpen]); + const handlePlanClick = ( + event: React.MouseEvent | KeyboardEvent, + ) => { + event.preventDefault(); + event.stopPropagation(); + + // Set conversation mode to "plan" immediately + setConversationMode("plan"); + + // Check if sub_conversation_ids is not empty + if ( + (conversation?.sub_conversation_ids && + conversation.sub_conversation_ids.length > 0) || + !conversation?.conversation_id + ) { + // Do nothing if both conditions are true + return; + } + + // Create a new sub-conversation if we have a current conversation ID + createConversation( + { + parentConversationId: conversation.conversation_id, + agentType: "plan", + }, + { + onSuccess: () => + displaySuccessToast( + t(I18nKey.PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED), + ), + }, + ); + }; + // Handle Shift + Tab keyboard shortcut to cycle through modes useEffect(() => { if (!shouldUsePlanningAgent || isAgentRunning) { @@ -52,7 +94,11 @@ export function ChangeAgentButton() { // Cycle between modes: code -> plan -> code const nextMode = conversationMode === "code" ? "plan" : "code"; - setConversationMode(nextMode); + if (nextMode === "plan") { + handlePlanClick(event); + } else { + setConversationMode(nextMode); + } } }; @@ -80,12 +126,6 @@ export function ChangeAgentButton() { setConversationMode("code"); }; - const handlePlanClick = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - setConversationMode("plan"); - }; - const isExecutionAgent = conversationMode === "code"; const buttonLabel = useMemo(() => { @@ -102,6 +142,8 @@ export function ChangeAgentButton() { return ; }, [isExecutionAgent]); + const isButtonDisabled = isAgentRunning || isCreatingConversation; + if (!shouldUsePlanningAgent) { return null; } @@ -111,11 +153,11 @@ export function ChangeAgentButton() {