feat(FN-2201): make Run Now available for active agents

- Show Run Now controls in AgentsView for active and running agents without requiring taskId
- Rename Run Heartbeat copy to Run Now and update runs-tab accessibility labels in AgentDetailView
- Add AgentsView coverage for Run Now visibility and startAgentRun invocation when no task is assigned
- Update agent runs UI static-analysis tests and add a patch changeset for @gsxdsm/fusion
This commit is contained in:
Fusion
2026-04-20 17:16:22 -07:00
committed by gsxdsm
parent c8203210c2
commit dfcb0868a2
5 changed files with 98 additions and 52 deletions

View File

@@ -1130,9 +1130,9 @@ function RunsTab({
<button
className="btn btn--sm btn--primary"
onClick={() => void handleRunHeartbeat()}
aria-label={`Run heartbeat for ${agentName ?? agentId}`}
aria-label={`Run now for ${agentName ?? agentId}`}
>
<Activity size={14} /> Run Heartbeat
<Activity size={14} /> Run Now
</button>
</div>
)}
@@ -1405,9 +1405,9 @@ function RunsTab({
<button
className="btn btn--sm btn--primary"
onClick={() => void handleRunHeartbeat()}
aria-label={`Run heartbeat for ${agentName ?? agentId}`}
aria-label={`Run now for ${agentName ?? agentId}`}
>
<Activity size={14} /> Run Heartbeat
<Activity size={14} /> Run Now
</button>
</div>
</div>

View File

@@ -730,16 +730,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
)}
{agent.state === "active" && (
<>
{agent.taskId && (
<button
className="btn btn--sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
title="Run Heartbeat"
aria-label={`Run heartbeat for ${agent.name}`}
>
<Activity size={14} />
</button>
)}
<button
className="btn btn--sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
title="Run Now"
aria-label={`Run now for ${agent.name}`}
>
<Activity size={14} />
</button>
<button
className="btn btn--sm"
onClick={() => void handleStateChange(agent.id, "paused")}
@@ -776,16 +774,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
)}
{agent.state === "running" && (
<>
{agent.taskId && (
<button
className="btn btn--sm"
disabled
title="Run in progress"
aria-label={`Heartbeat run in progress for ${agent.name}`}
>
<Activity size={14} />
</button>
)}
<button
className="btn btn--sm"
disabled
title="Run in progress"
aria-label={`Heartbeat run in progress for ${agent.name}`}
>
<Activity size={14} />
</button>
<button
className="btn btn--sm"
onClick={() => void handleStateChange(agent.id, "paused")}
@@ -990,16 +986,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
)}
{agent.state === "active" && (
<>
{agent.taskId && (
<button
className="btn btn--sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
title="Run Heartbeat"
aria-label={`Run heartbeat for ${agent.name}`}
>
<Activity size={14} /> Run
</button>
)}
<button
className="btn btn--sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
title="Run Now"
aria-label={`Run now for ${agent.name}`}
>
<Activity size={14} /> Run Now
</button>
<button
className="btn btn--sm"
onClick={() => void handleStateChange(agent.id, "paused")}
@@ -1036,16 +1030,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
)}
{agent.state === "running" && (
<>
{agent.taskId && (
<button
className="btn btn--sm"
disabled
title="Run in progress"
aria-label={`Heartbeat run in progress for ${agent.name}`}
>
<Activity size={14} /> Running
</button>
)}
<button
className="btn btn--sm"
disabled
title="Run in progress"
aria-label={`Heartbeat run in progress for ${agent.name}`}
>
<Activity size={14} /> Running
</button>
<button
className="btn btn--sm"
onClick={() => void handleStateChange(agent.id, "paused")}

View File

@@ -853,6 +853,55 @@ describe("AgentsView", () => {
});
});
describe("Run Now button", () => {
it("shows Run Now button for active agent without taskId", async () => {
const activeWithoutTaskId = { ...mockAgents[1] };
delete activeWithoutTaskId.taskId;
mockFetchAgents.mockResolvedValue([
mockAgents[0],
activeWithoutTaskId,
mockAgents[2],
mockAgents[3],
]);
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTitle("Run Now")).toBeTruthy();
});
});
it("Run Now button calls startAgentRun for active agent without taskId", async () => {
const activeWithoutTaskId = { ...mockAgents[1] };
delete activeWithoutTaskId.taskId;
mockFetchAgents.mockResolvedValue([
mockAgents[0],
activeWithoutTaskId,
mockAgents[2],
mockAgents[3],
]);
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTitle("Run Now")).toBeTruthy();
});
fireEvent.click(screen.getByTitle("Run Now"));
await waitFor(() => {
expect(mockStartAgentRun).toHaveBeenCalledWith(
"agent-002",
undefined,
expect.objectContaining({
source: "on_demand",
triggerDetail: "Triggered from dashboard",
}),
);
});
});
});
describe("delete agent", () => {
it("shows Delete button for idle and terminated agents in default view", async () => {
render(<AgentsView addToast={mockAddToast} />);