diff --git a/packages/e2e/tests/features/space-agent-centric-workflow.e2e.ts b/packages/e2e/tests/features/space-agent-centric-workflow.e2e.ts index 70a83de84..373fe3f83 100644 --- a/packages/e2e/tests/features/space-agent-centric-workflow.e2e.ts +++ b/packages/e2e/tests/features/space-agent-centric-workflow.e2e.ts @@ -114,6 +114,9 @@ test.describe('Agent-Centric Workflow', () => { const editor = page.getByTestId('visual-workflow-editor'); await editor.getByTestId('workflow-name-input').fill('Agent Channel Test'); + // Wait for the canvas to fully render before clicking add-step. + await expect(editor.getByTestId('add-step-button')).toBeVisible({ timeout: 5000 }); + // Add a node and configure two agents so agentRoles is populated, // which gives the ChannelEditor select dropdowns instead of text inputs. await editor.getByTestId('add-step-button').click(); @@ -169,6 +172,9 @@ test.describe('Agent-Centric Workflow', () => { const editor = page.getByTestId('visual-workflow-editor'); await editor.getByTestId('workflow-name-input').fill('Gate Config Test'); + // Wait for the canvas to fully render before clicking add-step. + await expect(editor.getByTestId('add-step-button')).toBeVisible({ timeout: 5000 }); + // Add a node with two agents so agentRoles is populated await editor.getByTestId('add-step-button').click(); const nodes = editor.locator('[data-testid^="workflow-node-"]').filter({ @@ -220,6 +226,9 @@ test.describe('Agent-Centric Workflow', () => { const editor = page.getByTestId('visual-workflow-editor'); await editor.getByTestId('workflow-name-input').fill(WORKFLOW_NAME); + // Wait for the canvas to fully render before clicking add-step. + await expect(editor.getByTestId('add-step-button')).toBeVisible({ timeout: 5000 }); + // Add a step with two agents await editor.getByTestId('add-step-button').click(); const nodes = editor.locator('[data-testid^="workflow-node-"]').filter({ @@ -239,8 +248,8 @@ test.describe('Agent-Centric Workflow', () => { const node = nodes.first(); const agentBadges = node.getByTestId('agent-badges'); await expect(agentBadges).toBeVisible({ timeout: 3000 }); - await expect(agentBadges.locator(`text=${AGENT_A_NAME}`)).toBeVisible({ timeout: 2000 }); - await expect(agentBadges.locator(`text=${AGENT_B_NAME}`)).toBeVisible({ timeout: 2000 }); + await expect(agentBadges.locator(`text=${ROLE_A}`)).toBeVisible({ timeout: 2000 }); + await expect(agentBadges.locator(`text=${ROLE_B}`)).toBeVisible({ timeout: 2000 }); // Without an active workflow run, no completion state icons should be visible // (no agent-status-spinner, agent-status-check, or agent-status-fail) @@ -268,8 +277,8 @@ test.describe('Agent-Centric Workflow', () => { const reopenedNode = reopenedNodes.first(); const reopenedBadges = reopenedNode.getByTestId('agent-badges'); await expect(reopenedBadges).toBeVisible({ timeout: 3000 }); - await expect(reopenedBadges.locator(`text=${AGENT_A_NAME}`)).toBeVisible({ timeout: 2000 }); - await expect(reopenedBadges.locator(`text=${AGENT_B_NAME}`)).toBeVisible({ timeout: 2000 }); + await expect(reopenedBadges.locator(`text=${ROLE_A}`)).toBeVisible({ timeout: 2000 }); + await expect(reopenedBadges.locator(`text=${ROLE_B}`)).toBeVisible({ timeout: 2000 }); // Still no completion state icons (no active run) await expect(reopenedNode.getByTestId('agent-status-spinner')).toHaveCount(0); @@ -288,6 +297,9 @@ test.describe('Agent-Centric Workflow', () => { const editor = page.getByTestId('visual-workflow-editor'); await editor.getByTestId('workflow-name-input').fill(WORKFLOW_NAME); + // Wait for the canvas to fully render before clicking add-step. + await expect(editor.getByTestId('add-step-button')).toBeVisible({ timeout: 5000 }); + // Add step with two agents await editor.getByTestId('add-step-button').click(); const nodes = editor.locator('[data-testid^="workflow-node-"]').filter({ diff --git a/packages/web/src/components/space/visual-editor/VisualWorkflowEditor.tsx b/packages/web/src/components/space/visual-editor/VisualWorkflowEditor.tsx index 82d781eb4..c0170d16c 100644 --- a/packages/web/src/components/space/visual-editor/VisualWorkflowEditor.tsx +++ b/packages/web/src/components/space/visual-editor/VisualWorkflowEditor.tsx @@ -362,18 +362,10 @@ export function VisualWorkflowEditor({ workflow, onSave, onCancel }: VisualWorkf // Node operations // ------------------------------------------------------------------ - function addStep() { + const addStep = useCallback(() => { const newLocalId = generateUUID(); const newStep: NodeDraft = { localId: newLocalId, name: '', agentId: '', instructions: '' }; - // Capture emptiness before the setNodes call so we can call setStartStepId - // outside the updater. State setter calls inside updater functions are side - // effects and violate the purity requirement (React StrictMode double-invokes - // updaters to catch exactly this pattern). - // Exclude the Task Agent virtual node — it is always present but not a real workflow step. - const isFirstNode = - nodes.filter((n) => n.step.id !== TASK_AGENT_NODE_ID && n.step.localId !== TASK_AGENT_NODE_ID) - .length === 0; setNodes((prev) => { // Stagger new nodes vertically so they don't overlap (nodes are ~160×80px). // Count only regular nodes so the Task Agent's fixed slot doesn't offset the stagger. @@ -381,10 +373,11 @@ export function VisualWorkflowEditor({ workflow, onSave, onCancel }: VisualWorkf (n) => n.step.id !== TASK_AGENT_NODE_ID && n.step.localId !== TASK_AGENT_NODE_ID ).length; const position: Point = { x: 120, y: 80 + regularCount * 100 }; + const isFirstNode = regularCount === 0; + if (isFirstNode) setStartStepId(newLocalId); return [...prev, { step: newStep, position }]; }); - if (isFirstNode) setStartStepId(newLocalId); - } + }, []); const handleNodePositionChange = useCallback((localId: string, newPosition: Point) => { // Task Agent is pinned — its position must never change.