feat(story-81): agent roster badges show availability state with green idle styling

- 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 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-23 15:43:25 +00:00
parent db60ed0d65
commit 0ef5e99d1b
2 changed files with 83 additions and 8 deletions

View File

@@ -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(<AgentPanel />);
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(<AgentPanel />);
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(<AgentPanel />);
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(<AgentPanel />);
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)");
});
});

View File

@@ -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 && (
<span
data-testid={`roster-dot-${agent.name}`}
style={{
width: "5px",
height: "5px",
@@ -130,20 +131,23 @@ function RosterBadge({
)}
{!isActive && (
<span
data-testid={`roster-dot-${agent.name}`}
style={{
width: "5px",
height: "5px",
borderRadius: "50%",
background: "#555",
background: "#3fb950",
flexShrink: 0,
}}
/>
)}
<span style={{ fontWeight: 600, color: isActive ? "#58a6ff" : "#aaa" }}>
<span
style={{ fontWeight: 600, color: isActive ? "#58a6ff" : "#3fb950" }}
>
{agent.name}
</span>
{agent.model && (
<span style={{ color: isActive ? "#7ab8ff" : "#666" }}>
<span style={{ color: isActive ? "#7ab8ff" : "#5ab96a" }}>
{agent.model}
</span>
)}
@@ -153,7 +157,7 @@ function RosterBadge({
</span>
)}
{!isActive && (
<span style={{ color: "#444", fontStyle: "italic" }}>idle</span>
<span style={{ color: "#5ab96a", fontStyle: "italic" }}>available</span>
)}
</span>
);