diff --git a/web/app/components/workflow/candidate-node-main.tsx b/web/app/components/workflow/candidate-node-main.tsx index d7473023891..50a46f89155 100644 --- a/web/app/components/workflow/candidate-node-main.tsx +++ b/web/app/components/workflow/candidate-node-main.tsx @@ -89,6 +89,7 @@ const CandidateNodeMain: FC = ({ } if (shouldCreateInlineAgentBinding) { + workflowStore.getState().setOpenInlineAgentPanelNodeId(candidateNode.id) createInlineAgentBinding(candidateNode.id, { onError: () => { const { nodes, setNodes } = collaborativeWorkflow.getState() @@ -105,7 +106,6 @@ const CandidateNodeMain: FC = ({ delete node.data._isTempNode } })) - workflowStore.getState().setOpenInlineAgentPanelNodeId(candidateNode.id) handleSyncWorkflowDraft(true, true) }, }) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 538c270692c..afd0bff2acc 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -193,6 +193,7 @@ export const useNodesInteractions = () => { const createInlineAgentBindingForNode = useCallback((nodeId: string, options?: { onError?: () => void }) => { + workflowStore.getState().setOpenInlineAgentPanelNodeId(nodeId) createInlineAgentBinding(nodeId, { onError: () => { options?.onError?.() @@ -208,7 +209,6 @@ export const useNodesInteractions = () => { delete node.data._isTempNode } })) - workflowStore.getState().setOpenInlineAgentPanelNodeId(nodeId) handleSyncWorkflowDraft(true, true) }, }) diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx index 1b824eb7510..723c6569ec7 100644 --- a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx @@ -27,7 +27,7 @@ const { mockHandleNodeDataUpdateWithSyncDraft: vi.fn((_payload, options) => options?.callback?.onSuccess?.()), mockInsertNodes: vi.fn(), mockOrchestrateDrawerPanelProps: [] as Array<{ - agentId: string + agentId?: string inlineComposerState?: unknown isInline: boolean nodeId: string @@ -150,7 +150,7 @@ vi.mock('../hooks', () => ({ vi.mock('../components/agent-orchestrate-drawer-panel', () => ({ AgentOrchestrateDrawerPanel: (props: { - agentId: string + agentId?: string appId?: string inlineComposerState?: unknown isInline: boolean @@ -321,6 +321,7 @@ describe('agent/panel', () => { }) it('renders a pending inline agent state while the binding is being created', () => { + mockStoreState.openInlineAgentPanelNodeId = 'agent-node' const { container } = render( { expect(screen.queryByText(/^workflow\.errorMsg\.fieldRequired/)).not.toBeInTheDocument() expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument() expect(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change' })).toBeDisabled() + expect(screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })).toBeInTheDocument() + expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument() + expect(screen.queryByText('workflow.nodes.agent.roster.editInConsole')).not.toBeInTheDocument() + expect(screen.queryByText('workflow.nodes.agent.roster.makeCopy')).not.toBeInTheDocument() + expect(mockOrchestrateDrawerPanelProps.at(-1)).toMatchObject({ + agentId: undefined, + isInline: true, + nodeId: 'agent-node', + open: true, + }) expect(screen.getByText('workflow.nodes.agent.task.label')).toBeInTheDocument() expect(screen.getByText('workflow.nodes.agent.outputVars.text')).toBeInTheDocument() expect(container.querySelector('[inert]')).toBeInTheDocument() @@ -570,10 +581,30 @@ describe('agent/panel', () => { fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change' })) fireEvent.click(screen.getByRole('button', { name: 'Start from Scratch' })) + expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('agent-node') expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith('agent-node', expect.objectContaining({ onSuccess: expect.any(Function), })) - expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('agent-node') + expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith( + { + id: 'agent-node', + data: expect.objectContaining({ + agent_binding: { + binding_type: 'inline_agent', + }, + agent_task: 'Keep this task', + agent_declared_outputs: [{ + name: 'summary', + type: 'string', + }], + _openInlineAgentPanel: true, + }), + }, + expect.objectContaining({ + sync: true, + notRefreshWhenSyncError: true, + }), + ) expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith( { id: 'agent-node', diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx index 0b174d02280..0dff1fe9583 100644 --- a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx +++ b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx @@ -15,7 +15,7 @@ import { consoleQuery } from '@/service/client' import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config' type AgentOrchestrateDrawerPanelProps = { - agentId: string + agentId?: string appId?: string inlineComposerState?: WorkflowAgentComposerResponse isInline: boolean @@ -40,7 +40,7 @@ function AgentOrchestrateDrawerPanelContent({ open, }: AgentOrchestrateDrawerPanelProps) { const rosterComposerQuery = useQuery(consoleQuery.agent.byAgentId.composer.get.queryOptions({ - input: open && !isInline + input: open && !isInline && agentId ? { params: { agent_id: agentId, @@ -62,12 +62,12 @@ function AgentOrchestrateDrawerPanelContent({ }) useHydrateAgentSoulConfigDraft({ - agentId: isInline ? `${nodeId}:${agentId}` : agentId, + agentId: isInline ? `${nodeId}:${agentId ?? 'pending'}` : agentId ?? nodeId, activeVersionId: activeConfigSnapshot?.id, config: agentSoulConfig as AgentSoulConfig | undefined, }) - if (!agentSoulConfig) { + if (!agentId || !agentSoulConfig) { return (
diff --git a/web/app/components/workflow/nodes/agent-v2/panel.tsx b/web/app/components/workflow/nodes/agent-v2/panel.tsx index c80c021407d..1a377d69341 100644 --- a/web/app/components/workflow/nodes/agent-v2/panel.tsx +++ b/web/app/components/workflow/nodes/agent-v2/panel.tsx @@ -40,18 +40,18 @@ export function AgentV2Panel({ const inlineAgentId = inputs.agent_binding?.binding_type === 'inline_agent' ? inputs.agent_binding.agent_id : undefined const isInlineAgentReady = hasValidInlineAgentBinding(inputs) const isInlineAgentPending = inputs.agent_binding?.binding_type === 'inline_agent' && !isInlineAgentReady - const isInlineAgentPanelOpen = isInlineAgentReady && openInlineAgentPanelNodeId === id + const isInlineAgentPanelOpen = (isInlineAgentReady || isInlineAgentPending) && openInlineAgentPanelNodeId === id const rosterAgentQuery = useAgentRosterDetail(rosterAgentId) const inlineAgentQuery = useWorkflowInlineAgentDetail(id, inlineAgentId) const { createInlineAgentBinding, isCreatingInlineAgent } = useCreateInlineAgentBinding() const inlineAgent = inlineAgentQuery.data?.agent - const isAgentPanelOpen = isInlineAgentReady ? isInlineAgentPanelOpen : isRosterAgentPanelOpen - const isInlineAgentLoading = isInlineAgentReady && !inlineAgent + const isAgentPanelOpen = isInlineAgentReady || isInlineAgentPending ? isInlineAgentPanelOpen : isRosterAgentPanelOpen + const isInlineAgentLoading = isInlineAgentPending || (isInlineAgentReady && !inlineAgent) const isAgentBindingPending = isInlineAgentPending || isCreatingInlineAgent const canStartFromScratch = inputs.agent_binding?.binding_type !== 'inline_agent' - const displayedAgent = rosterAgentQuery.data ?? (inlineAgentId && isInlineAgentReady + const displayedAgent = rosterAgentQuery.data ?? (isInlineAgentPending || isInlineAgentReady ? { - id: inlineAgentId, + id: inlineAgentId ?? id, name: inlineAgent?.name || t('nodes.agent.roster.inlineSetup.name', { ns: 'workflow' }), description: inlineAgent?.description, role: t('nodes.agent.roster.inlineSetup.type', { ns: 'workflow' }), @@ -114,12 +114,31 @@ export function AgentV2Panel({ }, [handleNodeDataUpdateWithSyncDraft, id, inputs, setOpenInlineAgentPanelNodeId]) const handleStartFromScratch = useCallback(() => { + setIsRosterAgentPanelOpen(false) + setIsInlineAgentPanelOpenedFromTrigger(false) + setOpenInlineAgentPanelNodeId(id) + + const pendingInputs = produce(inputsRef.current, (draft) => { + delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster + draft.agent_binding = { + binding_type: 'inline_agent', + } + draft._openInlineAgentPanel = true + }) + inputsRef.current = pendingInputs + handleNodeDataUpdateWithSyncDraft( + { + id, + data: pendingInputs, + }, + { + sync: true, + notRefreshWhenSyncError: true, + }, + ) + createInlineAgentBinding(id, { onSuccess: (binding) => { - setIsRosterAgentPanelOpen(false) - setIsInlineAgentPanelOpenedFromTrigger(false) - setOpenInlineAgentPanelNodeId(id) - const newInputs = produce(inputsRef.current, (draft) => { delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster draft.agent_binding = binding @@ -196,27 +215,27 @@ export function AgentV2Panel({
) : undefined} - panelMode={isInlineAgentReady && !isInlineAgentPanelOpenedFromTrigger ? 'setup' : 'detail'} + panelMode={isInlineAgentPending || (isInlineAgentReady && !isInlineAgentPanelOpenedFromTrigger) ? 'setup' : 'detail'} portalContainerRef={drawerPortalContainerRef} - showPanelDetailActions={!isInlineAgentReady} + showPanelDetailActions={!isInlineAgentReady && !isInlineAgentPending} onChange={handleRosterChange} onPanelOpenChange={handleAgentPanelOpenChange} onStartFromScratch={canStartFromScratch ? handleStartFromScratch : undefined} diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index e2937f28642..0628f225e8a 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -125,6 +125,7 @@ const AddBlock = ({ workflowStore.setState({ candidateNode: undefined, }) + workflowStore.getState().setOpenInlineAgentPanelNodeId(newNode.id) saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id }) createInlineAgentBinding(newNode.id, { onSuccess: (binding) => { @@ -138,7 +139,6 @@ const AddBlock = ({ delete node.data._isTempNode } })) - workflowStore.getState().setOpenInlineAgentPanelNodeId(newNode.id) handleSyncWorkflowDraft(true, true) }, })