From 29eff5118262ab854b314ca5f3a85545db51d9c1 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 16:04:02 +0000 Subject: [PATCH] story-kit: start 83_story_remove_active_work_list_from_agents_panel --- ...move_active_work_list_from_agents_panel.md | 20 + ...move_active_work_list_from_agents_panel.md | 2 +- frontend/src/components/AgentPanel.test.tsx | 330 +------------- frontend/src/components/AgentPanel.tsx | 430 ------------------ 4 files changed, 33 insertions(+), 749 deletions(-) create mode 100644 .story_kit/work/2_current/83_story_remove_active_work_list_from_agents_panel.md diff --git a/.story_kit/work/2_current/83_story_remove_active_work_list_from_agents_panel.md b/.story_kit/work/2_current/83_story_remove_active_work_list_from_agents_panel.md new file mode 100644 index 0000000..2223dfe --- /dev/null +++ b/.story_kit/work/2_current/83_story_remove_active_work_list_from_agents_panel.md @@ -0,0 +1,20 @@ +--- +name: "Remove active work list from Agents panel" +test_plan: approved +--- + +# Story 83: Remove active work list from Agents panel + +## User Story + +As a user, I want the Agents panel to only show the agent roster (with lozenges) and not a separate list of active work items, since the flying lozenges already indicate which agents are working on which stories. + +## Acceptance Criteria + +- [ ] The Agents panel no longer displays the list of active work items / agents-at-work section +- [ ] The agent roster with availability lozenges remains visible and functional +- [ ] Flying lozenge animations continue to work as before + +## Out of Scope + +- TBD diff --git a/.story_kit/work/4_merge/83_story_remove_active_work_list_from_agents_panel.md b/.story_kit/work/4_merge/83_story_remove_active_work_list_from_agents_panel.md index 1ff5086..2223dfe 100644 --- a/.story_kit/work/4_merge/83_story_remove_active_work_list_from_agents_panel.md +++ b/.story_kit/work/4_merge/83_story_remove_active_work_list_from_agents_panel.md @@ -1,6 +1,6 @@ --- name: "Remove active work list from Agents panel" -test_plan: pending +test_plan: approved --- # Story 83: Remove active work list from Agents panel diff --git a/frontend/src/components/AgentPanel.test.tsx b/frontend/src/components/AgentPanel.test.tsx index da49af1..211cfda 100644 --- a/frontend/src/components/AgentPanel.test.tsx +++ b/frontend/src/components/AgentPanel.test.tsx @@ -1,14 +1,5 @@ -import { act, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; +import { render, screen } from "@testing-library/react"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentConfigInfo, AgentInfo } from "../api/agents"; import { agentsApi } from "../api/agents"; @@ -43,7 +34,7 @@ const ROSTER: AgentConfigInfo[] = [ }, ]; -describe("AgentPanel diff command", () => { +describe("AgentPanel active work list removed", () => { beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); @@ -53,325 +44,28 @@ describe("AgentPanel diff command", () => { mockedAgents.listAgents.mockResolvedValue([]); }); - it("shows diff command when an agent has a worktree path", async () => { + it("does not render active agent entries even when agents are running", async () => { const agentList: AgentInfo[] = [ { - story_id: "33_diff_commands", + story_id: "83_active", agent_name: "coder-1", status: "running", session_id: null, - worktree_path: "/tmp/project-story-33", + worktree_path: "/tmp/wt", base_branch: "master", }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); - render(); + const { container } = render(); - // Expand the agent detail by clicking the expand button - const expandButton = await screen.findByText("▶"); - await userEvent.click(expandButton); + // Roster badge should still be visible + await screen.findByTestId("roster-badge-coder-1"); - // The diff command should be rendered + // No agent entry divs should exist expect( - await screen.findByText( - 'cd "/tmp/project-story-33" && git difftool master...HEAD', - ), - ).toBeInTheDocument(); - - // A copy button should exist - expect(screen.getByText("Copy")).toBeInTheDocument(); - }); - - it("copies diff command to clipboard on click", async () => { - const writeText = vi.fn().mockResolvedValue(undefined); - Object.assign(navigator, { - clipboard: { writeText }, - }); - - const agentList: AgentInfo[] = [ - { - story_id: "33_diff_commands", - agent_name: "coder-1", - status: "completed", - session_id: null, - worktree_path: "/home/user/my-project-story-33", - base_branch: "main", - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - render(); - - const expandButton = await screen.findByText("▶"); - await userEvent.click(expandButton); - - const copyButton = await screen.findByText("Copy"); - await userEvent.click(copyButton); - - await waitFor(() => { - expect(writeText).toHaveBeenCalledWith( - 'cd "/home/user/my-project-story-33" && git difftool main...HEAD', - ); - }); - - expect(await screen.findByText("Copied")).toBeInTheDocument(); - }); - - it("uses base_branch from the server in the diff command", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "33_diff_commands", - agent_name: "coder-1", - status: "running", - session_id: null, - worktree_path: "/tmp/wt", - base_branch: "develop", - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - render(); - - const expandButton = await screen.findByText("▶"); - await userEvent.click(expandButton); - - expect( - await screen.findByText('cd "/tmp/wt" && git difftool develop...HEAD'), - ).toBeInTheDocument(); - }); - - it("defaults to master when base_branch is null", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "33_diff_commands", - agent_name: "coder-1", - status: "running", - session_id: null, - worktree_path: "/tmp/wt", - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - render(); - - const expandButton = await screen.findByText("▶"); - await userEvent.click(expandButton); - - expect( - await screen.findByText('cd "/tmp/wt" && git difftool master...HEAD'), - ).toBeInTheDocument(); - }); -}); - -describe("AgentPanel fade-out", () => { - beforeAll(() => { - Element.prototype.scrollIntoView = vi.fn(); - }); - - beforeEach(() => { - mockedAgents.getAgentConfig.mockResolvedValue(ROSTER); - mockedAgents.listAgents.mockResolvedValue([]); - }); - - it("applies fade animation to a completed agent", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "73_fade_test", - agent_name: "coder-1", - status: "completed", - session_id: null, - worktree_path: null, - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - const { container } = render(); - - const entry = await waitFor(() => { - const el = container.querySelector( - '[data-testid="agent-entry-73_fade_test:coder-1"]', - ); - expect(el).toBeInTheDocument(); - return el as HTMLElement; - }); - - expect(entry.style.animationName).toBe("agentFadeOut"); - }); - - it("applies fade animation to a failed agent", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "73_fade_fail", - agent_name: "coder-1", - status: "failed", - session_id: null, - worktree_path: null, - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - const { container } = render(); - - const entry = await waitFor(() => { - const el = container.querySelector( - '[data-testid="agent-entry-73_fade_fail:coder-1"]', - ); - expect(el).toBeInTheDocument(); - return el as HTMLElement; - }); - - expect(entry.style.animationName).toBe("agentFadeOut"); - }); - - it("does not apply fade animation to a running agent", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "73_running", - agent_name: "coder-1", - status: "running", - session_id: null, - worktree_path: null, - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - const { container } = render(); - - const entry = await waitFor(() => { - const el = container.querySelector( - '[data-testid="agent-entry-73_running:coder-1"]', - ); - expect(el).toBeInTheDocument(); - return el as HTMLElement; - }); - - expect(entry.style.animationName).not.toBe("agentFadeOut"); - }); - - it("pauses the fade when the entry is expanded", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "73_pause_test", - agent_name: "coder-1", - status: "completed", - session_id: null, - worktree_path: null, - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - const { container } = render(); - - const entry = await waitFor(() => { - const el = container.querySelector( - '[data-testid="agent-entry-73_pause_test:coder-1"]', - ); - expect(el).toBeInTheDocument(); - return el as HTMLElement; - }); - - // Initially running (not paused) - expect(entry.style.animationPlayState).toBe("running"); - - // Expand the agent - const expandButton = screen.getByRole("button", { name: "▶" }); - await userEvent.click(expandButton); - - // Animation should be paused - expect(entry.style.animationPlayState).toBe("paused"); - }); - - it("resumes the fade when the entry is collapsed", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "73_resume_test", - agent_name: "coder-1", - status: "completed", - session_id: null, - worktree_path: null, - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - const { container } = render(); - - const entry = await waitFor(() => { - const el = container.querySelector( - '[data-testid="agent-entry-73_resume_test:coder-1"]', - ); - expect(el).toBeInTheDocument(); - return el as HTMLElement; - }); - - const expandButton = screen.getByRole("button", { name: "▶" }); - - // Expand - await userEvent.click(expandButton); - expect(entry.style.animationPlayState).toBe("paused"); - - // Collapse - await userEvent.click(expandButton); - expect(entry.style.animationPlayState).toBe("running"); - }); - - describe("removes the agent entry after 60s", () => { - beforeEach(() => { - vi.useFakeTimers({ shouldAdvanceTime: true }); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("removes the agent entry after the 60-second fade completes", async () => { - const agentList: AgentInfo[] = [ - { - story_id: "73_remove_test", - agent_name: "coder-1", - status: "completed", - session_id: null, - worktree_path: null, - base_branch: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - const { container } = render(); - - // With fake timers active, waitFor's polling setInterval never fires. - // Use act to flush pending promises and React state updates instead. - await act(async () => { - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - }); - - expect( - container.querySelector( - '[data-testid="agent-entry-73_remove_test:coder-1"]', - ), - ).toBeInTheDocument(); - - // Advance timers by 60 seconds and flush React state updates - await act(async () => { - vi.advanceTimersByTime(60_000); - }); - - // Entry should be removed - expect( - container.querySelector( - '[data-testid="agent-entry-73_remove_test:coder-1"]', - ), - ).not.toBeInTheDocument(); - }); + container.querySelector('[data-testid^="agent-entry-"]'), + ).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/components/AgentPanel.tsx b/frontend/src/components/AgentPanel.tsx index 0cd44ae..01105bc 100644 --- a/frontend/src/components/AgentPanel.tsx +++ b/frontend/src/components/AgentPanel.tsx @@ -20,20 +20,6 @@ interface AgentState { terminalAt: number | null; } -const STATUS_COLORS: Record = { - pending: "#e3b341", - running: "#58a6ff", - completed: "#7ee787", - failed: "#ff7b72", -}; - -const STATUS_LABELS: Record = { - pending: "Pending", - running: "Running", - completed: "Completed", - failed: "Failed", -}; - const formatTimestamp = (value: Date | null): string => { if (!value) return ""; return value.toLocaleTimeString([], { @@ -43,38 +29,6 @@ const formatTimestamp = (value: Date | null): string => { }); }; -function StatusBadge({ status }: { status: AgentStatusValue }) { - return ( - - {status === "running" && ( - - )} - {STATUS_LABELS[status]} - - ); -} - function RosterBadge({ agent, activeStoryId, @@ -168,157 +122,15 @@ function agentKey(storyId: string, agentName: string): string { return `${storyId}:${agentName}`; } -function DiffCommand({ - worktreePath, - baseBranch, -}: { - worktreePath: string; - baseBranch: string; -}) { - const [copied, setCopied] = useState(false); - const command = `cd "${worktreePath}" && git difftool ${baseBranch}...HEAD`; - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(command); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - // Fallback: select text for manual copy - } - }; - - return ( -
- - {command} - - -
- ); -} - -export function EditorCommand({ - worktreePath, - editorCommand, -}: { - worktreePath: string; - editorCommand: string; -}) { - const [copied, setCopied] = useState(false); - const command = `${editorCommand} "${worktreePath}"`; - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(command); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - // Fallback: select text for manual copy - } - }; - - return ( -
- - {command} - - -
- ); -} - -const FADE_DURATION_MS = 60_000; - export function AgentPanel() { const [agents, setAgents] = useState>({}); const [roster, setRoster] = useState([]); - const [expandedKey, setExpandedKey] = useState(null); const [actionError, setActionError] = useState(null); const [lastRefresh, setLastRefresh] = useState(null); const [editorCommand, setEditorCommand] = useState(null); const [editorInput, setEditorInput] = useState(""); const [editingEditor, setEditingEditor] = useState(false); const cleanupRefs = useRef void>>({}); - const logEndRefs = useRef>({}); - // Refs for fade-out timers (pause/resume on expand/collapse) - const fadeTimerRef = useRef>>( - {}, - ); - const fadeElapsedRef = useRef>({}); - const fadeTimerStartRef = useRef>({}); // Load roster, existing agents, and editor preference on mount useEffect(() => { @@ -449,79 +261,6 @@ export function AgentPanel() { cleanupRefs.current[key] = cleanup; }, []); - // Auto-scroll log when expanded - useEffect(() => { - if (expandedKey) { - const el = logEndRefs.current[expandedKey]; - el?.scrollIntoView({ behavior: "smooth" }); - } - }, [expandedKey, agents]); - - // Manage fade-out timers for terminal agents. - // Timers are paused when an entry is expanded and resumed on collapse. - useEffect(() => { - for (const [key, agent] of Object.entries(agents)) { - if (!agent.terminalAt) continue; - const isExpanded = expandedKey === key; - const hasTimer = key in fadeTimerRef.current; - - if (isExpanded && hasTimer) { - // Pause: clear timer and accumulate elapsed time - clearTimeout(fadeTimerRef.current[key]); - const started = fadeTimerStartRef.current[key]; - if (started !== null && started !== undefined) { - fadeElapsedRef.current[key] = - (fadeElapsedRef.current[key] ?? 0) + (Date.now() - started); - } - delete fadeTimerRef.current[key]; - fadeTimerStartRef.current[key] = null; - } else if (!isExpanded && !hasTimer) { - // Start or resume timer with remaining time - const elapsed = fadeElapsedRef.current[key] ?? 0; - const remaining = Math.max(0, FADE_DURATION_MS - elapsed); - fadeTimerStartRef.current[key] = Date.now(); - fadeTimerRef.current[key] = setTimeout(() => { - setAgents((prev) => { - const next = { ...prev }; - delete next[key]; - return next; - }); - delete fadeTimerRef.current[key]; - delete fadeTimerStartRef.current[key]; - delete fadeElapsedRef.current[key]; - }, remaining); - } - } - }, [agents, expandedKey]); - - // Clean up fade timers on unmount - useEffect(() => { - return () => { - for (const timer of Object.values(fadeTimerRef.current)) { - clearTimeout(timer); - } - fadeTimerRef.current = {}; - }; - }, []); - - const handleStop = async (storyId: string, agentName: string) => { - setActionError(null); - const key = agentKey(storyId, agentName); - try { - await agentsApi.stopAgent(storyId, agentName); - cleanupRefs.current[key]?.(); - delete cleanupRefs.current[key]; - setAgents((prev) => { - const next = { ...prev }; - delete next[key]; - return next; - }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - setActionError(`Failed to stop agent for ${storyId}: ${message}`); - } - }; - const handleSaveEditor = async () => { try { const trimmed = editorInput.trim() || null; @@ -699,175 +438,6 @@ export function AgentPanel() { {actionError} )} - - {/* Active agents */} - {Object.entries(agents).length > 0 && ( -
- {Object.entries(agents).map(([key, a]) => ( -
-
- - -
- {a.agentName} - - {key.split(":")[0]} - -
- - - - {(a.status === "running" || a.status === "pending") && ( - - )} -
- - {expandedKey === key && ( -
- {a.worktreePath && ( -
- Worktree: {a.worktreePath} -
- )} - {a.worktreePath && ( - - )} -
- {a.log.length === 0 ? ( - - {a.status === "pending" || a.status === "running" - ? "Waiting for output..." - : "No output captured."} - - ) : ( - a.log.map((line, i) => ( -
- {line} -
- )) - )} -
{ - logEndRefs.current[key] = el; - }} - /> -
-
- )} -
- ))} -
- )}
); }