From 0ef5e99d1be524b19ffd6370e1017da02ed366dc Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 15:43:25 +0000 Subject: [PATCH] feat(story-81): agent roster badges show availability state with green idle styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add data-testid="roster-dot-{name}" to both active and idle dot spans for testability - Change idle badge from grey (#888, #555, #333) to green (#3fb950, #3fb95015, #3fb95040) - Update idle label from "idle" to "available" to reinforce positive availability signal - Update tooltip from "— idle" to "— available" for consistency - Active/running agents retain blue (#58a6ff) pulsing dot styling unchanged - Add 4 new Vitest tests covering green idle dot, green badge styling, blue active dot, and blue active badge Closes story 81: Agent roster badges show availability state Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/AgentPanel.test.tsx | 71 +++++++++++++++++++++ frontend/src/components/AgentPanel.tsx | 20 +++--- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/AgentPanel.test.tsx b/frontend/src/components/AgentPanel.test.tsx index f9c79dd..da49af1 100644 --- a/frontend/src/components/AgentPanel.test.tsx +++ b/frontend/src/components/AgentPanel.test.tsx @@ -374,3 +374,74 @@ describe("AgentPanel fade-out", () => { }); }); }); + +describe("RosterBadge availability state", () => { + beforeAll(() => { + Element.prototype.scrollIntoView = vi.fn(); + }); + + beforeEach(() => { + mockedAgents.getAgentConfig.mockResolvedValue(ROSTER); + mockedAgents.listAgents.mockResolvedValue([]); + }); + + it("shows a green dot for an idle agent", async () => { + render(); + + const dot = await screen.findByTestId("roster-dot-coder-1"); + // JSDOM normalizes #3fb950 to rgb(63, 185, 80) + expect(dot.style.background).toBe("rgb(63, 185, 80)"); + expect(dot.style.animation).toBe(""); + }); + + it("shows green badge styling for an idle agent", async () => { + render(); + + const badge = await screen.findByTestId("roster-badge-coder-1"); + // JSDOM normalizes #3fb95015 to rgba(63, 185, 80, 0.082) and #3fb950 to rgb(63, 185, 80) + expect(badge.style.background).toBe("rgba(63, 185, 80, 0.082)"); + expect(badge.style.color).toBe("rgb(63, 185, 80)"); + }); + + it("shows a blue pulsing dot for an active agent", async () => { + const agentList: AgentInfo[] = [ + { + story_id: "81_active", + agent_name: "coder-1", + status: "running", + session_id: null, + worktree_path: null, + base_branch: null, + }, + ]; + mockedAgents.listAgents.mockResolvedValue(agentList); + + render(); + + const dot = await screen.findByTestId("roster-dot-coder-1"); + // JSDOM normalizes #58a6ff to rgb(88, 166, 255) + expect(dot.style.background).toBe("rgb(88, 166, 255)"); + expect(dot.style.animation).toBe("pulse 1.5s infinite"); + }); + + it("shows blue badge styling for an active agent", async () => { + const agentList: AgentInfo[] = [ + { + story_id: "81_active", + agent_name: "coder-1", + status: "running", + session_id: null, + worktree_path: null, + base_branch: null, + }, + ]; + mockedAgents.listAgents.mockResolvedValue(agentList); + + render(); + + const badge = await screen.findByTestId("roster-badge-coder-1"); + // JSDOM normalizes #58a6ff18 to rgba(88, 166, 255, 0.094) and #58a6ff to rgb(88, 166, 255) + expect(badge.style.background).toBe("rgba(88, 166, 255, 0.094)"); + expect(badge.style.color).toBe("rgb(88, 166, 255)"); + }); +}); diff --git a/frontend/src/components/AgentPanel.tsx b/frontend/src/components/AgentPanel.tsx index 154edaa..0cd44ae 100644 --- a/frontend/src/components/AgentPanel.tsx +++ b/frontend/src/components/AgentPanel.tsx @@ -105,19 +105,20 @@ function RosterBadge({ padding: "2px 8px", borderRadius: "6px", fontSize: "0.7em", - background: isActive ? "#58a6ff18" : "#ffffff08", - color: isActive ? "#58a6ff" : "#888", - border: isActive ? "1px solid #58a6ff44" : "1px solid #333", + background: isActive ? "#58a6ff18" : "#3fb95015", + color: isActive ? "#58a6ff" : "#3fb950", + border: isActive ? "1px solid #58a6ff44" : "1px solid #3fb95040", transition: "background 0.3s, color 0.3s, border-color 0.3s", }} title={ isActive ? `Working on #${storyNumber ?? activeStoryId}` - : `${agent.role || agent.name} — idle` + : `${agent.role || agent.name} — available` } > {isActive && ( )} - + {agent.name} {agent.model && ( - + {agent.model} )} @@ -153,7 +157,7 @@ function RosterBadge({ )} {!isActive && ( - idle + available )} );