feat(FN-2116): add start action for terminated agent cards

- Add a Start button for terminated agents in board cards alongside Delete
- Add a Start action for terminated agents in list cards with icon + label
- Reuse the existing state transition handler to move terminated agents back to active
- Extend AgentListModal tests to cover Start button visibility and start action behavior
This commit is contained in:
Fusion
2026-04-19 02:44:48 -07:00
committed by gsxdsm
parent 46aeee0959
commit f947b7dc49
2 changed files with 80 additions and 14 deletions

View File

@@ -792,13 +792,22 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
</>
)}
{agent.state === "terminated" && (
<button
className="btn btn--sm btn--danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} />
</button>
<>
<button
className="btn btn--sm"
onClick={() => void handleStateChange(agent.id, "active")}
title="Start"
>
<Play size={14} />
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} />
</button>
</>
)}
</div>
</div>
@@ -1023,13 +1032,22 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
</>
)}
{agent.state === "terminated" && (
<button
className="btn btn--sm btn--danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} /> Delete
</button>
<>
<button
className="btn btn--sm"
onClick={() => void handleStateChange(agent.id, "active")}
title="Start"
>
<Play size={14} /> Start
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} /> Delete
</button>
</>
)}
</div>
</div>

View File

@@ -564,6 +564,54 @@ describe("AgentListModal", () => {
});
});
it("shows Start button for terminated agents", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByTitle("Start")).toBeTruthy();
});
});
it("starts terminated agent", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByTitle("Start")).toBeTruthy();
});
fireEvent.click(screen.getByTitle("Start"));
await waitFor(() => {
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-004", "active", undefined);
});
});
it("handles state change errors gracefully", async () => {
mockUpdateAgentState.mockRejectedValue(new Error("Invalid transition"));