diff --git a/src/__tests__/unit/components/features/CompactTasksList.test.tsx b/src/__tests__/unit/components/features/CompactTasksList.test.tsx index 9e30bc9af..19b6f3ad6 100644 --- a/src/__tests__/unit/components/features/CompactTasksList.test.tsx +++ b/src/__tests__/unit/components/features/CompactTasksList.test.tsx @@ -1235,6 +1235,74 @@ describe("CompactTasksList", () => { const graph = screen.getByTestId("dependency-graph"); expect(graph.className).toContain("h-[380px]"); }); + + test("auto-opens graph when tasks transition from 0 to N (post-generation)", () => { + (useIsMobile as any).mockReturnValue(false); + const feature = createMockFeature([]); + + const { rerender } = render( + + ); + + // Initially no tasks → hasDependencies = false → collapsible not rendered + expect(screen.queryByTestId("collapsible")).not.toBeInTheDocument(); + + // Tasks arrive via Pusher / generation + const task1 = createMockTask({ id: "t1", dependsOnTaskIds: [] }); + const task2 = createMockTask({ id: "t2", dependsOnTaskIds: ["t1"] }); + const updatedFeature = createMockFeature([task1, task2]); + + rerender( + + ); + + expect(screen.getByTestId("collapsible")).toHaveAttribute("data-open", "true"); + }); + + test("does not auto-open graph when adding a task to an already-populated list (N → N+1)", () => { + (useIsMobile as any).mockReturnValue(false); + // Start with 1 task — graphOpen initialises to false + const task1 = createMockTask({ id: "t1", dependsOnTaskIds: [] }); + const feature = createMockFeature([task1]); + + const { rerender } = render( + + ); + + // Initially no dependencies on the 1 task → hasDependencies = false → collapsible not rendered + expect(screen.queryByTestId("collapsible")).not.toBeInTheDocument(); + + // A second task is added (1 → 2, not 0 → N) + const task2 = createMockTask({ id: "t2", dependsOnTaskIds: ["t1"] }); + const updatedFeature = createMockFeature([task1, task2]); + + rerender( + + ); + + // Graph should remain closed — effect only fires on 0 → N transition + expect(screen.getByTestId("collapsible")).toHaveAttribute("data-open", "false"); + }); }); describe("Pusher feature channel subscription", () => { diff --git a/src/components/features/CompactTasksList/index.tsx b/src/components/features/CompactTasksList/index.tsx index 295ce27b0..b159a44c0 100644 --- a/src/components/features/CompactTasksList/index.tsx +++ b/src/components/features/CompactTasksList/index.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useMemo, useCallback, useEffect } from "react"; +import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { ChevronDown, ExternalLink, Play, Trash2, RefreshCw, Copy, Sparkles, CheckCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -125,6 +125,14 @@ export function CompactTasksList({ featureId, feature, onUpdate, isGenerating }: const [graphOpen, setGraphOpen] = useState(tasks.length > 1); + const prevTasksLengthRef = useRef(tasks.length); + useEffect(() => { + if (prevTasksLengthRef.current === 0 && tasks.length > 0) { + setGraphOpen(true); + } + prevTasksLengthRef.current = tasks.length; + }, [tasks.length]); + const hasDependencies = useMemo( () => tasks.some((t) => (t.dependsOnTaskIds ?? []).length > 0), [tasks]