feat(FN-1664): merge fusion/fn-1664 (auto-resolved)

- docs(FN-1664): add changeset for Active Agents card selection fix
- test(FN-1664): add regression tests for active agent card selection
- feat(FN-1664): wire ActiveAgentsPanel selection to open AgentDetailView
This commit is contained in:
gsxdsm
2026-04-13 00:31:54 -07:00
parent 65c15c56b6
commit a3e154dc31
4 changed files with 185 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
---
"@gsxdsm/fusion": patch
---
Fix Active Agents card selection so spawned agents can open AgentDetailView. Added click and keyboard (Enter/Space) support to LiveAgentCard components with proper accessibility attributes (role="button", tabIndex=0, aria-label).

View File

@@ -4,16 +4,37 @@ import { useLiveTranscript } from "../hooks/useLiveTranscript";
interface LiveAgentCardProps {
agent: Agent;
onSelect?: (agentId: string) => void;
}
function LiveAgentCard({ agent }: LiveAgentCardProps) {
function LiveAgentCard({ agent, onSelect }: LiveAgentCardProps) {
const { entries, isConnected } = useLiveTranscript(agent.taskId);
const elapsed = agent.lastHeartbeatAt
? Math.floor((Date.now() - new Date(agent.lastHeartbeatAt).getTime()) / 1000)
: 0;
const handleSelect = () => {
if (onSelect) {
onSelect(agent.id);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect();
}
};
return (
<div className="live-agent-card">
<div
className="live-agent-card"
onClick={handleSelect}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
aria-label={`Select agent ${agent.name}`}
>
<div className="live-agent-card-header">
<div className="live-agent-card-name">
<span className="live-agent-pulse" />
@@ -52,9 +73,10 @@ function formatElapsed(seconds: number): string {
interface ActiveAgentsPanelProps {
agents: Agent[];
onAgentSelect?: (agentId: string) => void;
}
export function ActiveAgentsPanel({ agents }: ActiveAgentsPanelProps) {
export function ActiveAgentsPanel({ agents, onAgentSelect }: ActiveAgentsPanelProps) {
if (agents.length === 0) return null;
return (
@@ -65,7 +87,7 @@ export function ActiveAgentsPanel({ agents }: ActiveAgentsPanelProps) {
</div>
<div className="active-agents-grid">
{agents.map(agent => (
<LiveAgentCard key={agent.id} agent={agent} />
<LiveAgentCard key={agent.id} agent={agent} onSelect={onAgentSelect} />
))}
</div>
</div>

View File

@@ -496,7 +496,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
<AgentMetricsBar stats={stats} />
{/* Active Agents Panel - Live streaming cards */}
<ActiveAgentsPanel agents={activeAgents} />
<ActiveAgentsPanel agents={activeAgents} onAgentSelect={setSelectedAgentId} />
{/* Agent List */}
{agentView === "tree" ? (

View File

@@ -786,4 +786,157 @@ describe("AgentsView", () => {
});
});
});
describe("active agents panel selection", () => {
it("renders active agents panel when agents are active", async () => {
// agent-002 is active with taskId FN-001
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
});
// Should have a live agent card for the active agent
const liveAgentCards = document.querySelectorAll(".live-agent-card");
expect(liveAgentCards.length).toBe(1);
});
it("opens AgentDetailView when clicking an active agent card", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
});
// Find and click the live agent card
const liveAgentCard = document.querySelector(".live-agent-card");
expect(liveAgentCard).toBeTruthy();
fireEvent.click(liveAgentCard!);
await waitFor(() => {
// Should open detail view for agent-002 (the active agent)
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("agent-002");
});
});
it("opens AgentDetailView when pressing Enter on an active agent card", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
});
// Find the live agent card
const liveAgentCard = document.querySelector(".live-agent-card") as HTMLElement;
expect(liveAgentCard).toBeTruthy();
// Focus and press Enter
liveAgentCard.focus();
fireEvent.keyDown(liveAgentCard, { key: "Enter" });
await waitFor(() => {
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("agent-002");
});
});
it("opens AgentDetailView when pressing Space on an active agent card", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
});
// Find the live agent card
const liveAgentCard = document.querySelector(".live-agent-card") as HTMLElement;
expect(liveAgentCard).toBeTruthy();
// Focus and press Space
liveAgentCard.focus();
fireEvent.keyDown(liveAgentCard, { key: " " });
await waitFor(() => {
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("agent-002");
});
});
it("live agent cards have proper accessibility attributes", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
});
const liveAgentCard = document.querySelector(".live-agent-card") as HTMLElement;
expect(liveAgentCard).toBeTruthy();
// Check accessibility attributes
expect(liveAgentCard.getAttribute("role")).toBe("button");
expect(liveAgentCard.getAttribute("tabIndex")).toBe("0");
expect(liveAgentCard.getAttribute("aria-label")).toBe("Select agent Test Agent 2");
});
it("does not show active agents panel when no agents are active", async () => {
// Create agents with no active ones
const inactiveAgents: Agent[] = [
{
id: "agent-005",
name: "Idle Agent",
role: "executor" as AgentCapability,
state: "idle" as AgentState,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
];
mockFetchAgents.mockResolvedValue(inactiveAgents);
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.queryByText("Active Agents")).toBeNull();
});
});
it("opens AgentDetailView for spawned agents in the active panel", async () => {
// Simulate spawned agents by having multiple active agents
const spawnedAgents: Agent[] = [
...mockAgents,
{
id: "spawned-001",
name: "Spawned Worker",
role: "custom" as AgentCapability,
state: "active" as AgentState,
taskId: "FN-100",
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
];
mockFetchAgents.mockResolvedValue(spawnedAgents);
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Active Agents (2)")).toBeTruthy();
});
// Find and click the spawned agent card
const liveAgentCards = document.querySelectorAll(".live-agent-card");
expect(liveAgentCards.length).toBe(2);
// Click on the spawned agent
const spawnedCard = Array.from(liveAgentCards).find(
card => card.textContent?.includes("Spawned Worker")
);
expect(spawnedCard).toBeTruthy();
fireEvent.click(spawnedCard!);
await waitFor(() => {
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("spawned-001");
});
});
});
});