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:
5
.changeset/fix-spawned-agent-card-selection.md
Normal file
5
.changeset/fix-spawned-agent-card-selection.md
Normal 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).
|
||||
@@ -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>
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user