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:
5
.changeset/add-run-now-button.md
Normal file
5
.changeset/add-run-now-button.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@gsxdsm/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a "Run Now" action for active agents in the dashboard and remove the taskId gate so on-demand runs can be triggered even when no task is currently assigned.
|
||||||
@@ -34,7 +34,7 @@ describe("Agent runs UI — static analysis", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("AgentsView Run Heartbeat button", () => {
|
describe("AgentsView Run Now button", () => {
|
||||||
it("has handleRunHeartbeat function", () => {
|
it("has handleRunHeartbeat function", () => {
|
||||||
expect(agentsViewContent).toContain("handleRunHeartbeat");
|
expect(agentsViewContent).toContain("handleRunHeartbeat");
|
||||||
});
|
});
|
||||||
@@ -44,12 +44,12 @@ describe("Agent runs UI — static analysis", () => {
|
|||||||
expect(agentsViewContent).toMatch(/startAgentRun.*Triggered from dashboard/);
|
expect(agentsViewContent).toMatch(/startAgentRun.*Triggered from dashboard/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows Run Heartbeat button for active agents with taskId", () => {
|
it("shows Run Now button for active agents", () => {
|
||||||
// The button should appear in the active state block
|
// The button should appear in the active state block
|
||||||
// and should be conditioned on agent.taskId
|
// and always be available for active agents.
|
||||||
const activeBlock = agentsViewContent.match(/agent\.state === "active"[\s\S]*?agent\.state === "paused"/)?.[0] ?? "";
|
const activeBlock = agentsViewContent.match(/agent\.state === "active"[\s\S]*?agent\.state === "paused"/)?.[0] ?? "";
|
||||||
expect(activeBlock).toContain("handleRunHeartbeat");
|
expect(activeBlock).toContain("handleRunHeartbeat");
|
||||||
expect(activeBlock).toContain("agent.taskId");
|
expect(activeBlock).toContain("Run Now");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows disabled button for running agents", () => {
|
it("shows disabled button for running agents", () => {
|
||||||
@@ -57,7 +57,7 @@ describe("Agent runs UI — static analysis", () => {
|
|||||||
expect(runningBlock).toContain("disabled");
|
expect(runningBlock).toContain("disabled");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses Activity icon for the Run Heartbeat button", () => {
|
it("uses Activity icon for the Run Now button", () => {
|
||||||
expect(agentsViewContent).toContain("handleRunHeartbeat");
|
expect(agentsViewContent).toContain("handleRunHeartbeat");
|
||||||
// Activity icon is imported and used in run heartbeat buttons
|
// Activity icon is imported and used in run heartbeat buttons
|
||||||
expect(agentsViewContent).toMatch(/from.*lucide-react/);
|
expect(agentsViewContent).toMatch(/from.*lucide-react/);
|
||||||
@@ -74,12 +74,12 @@ describe("Agent runs UI — static analysis", () => {
|
|||||||
expect(agentDetailViewContent).toMatch(/fetchAgentRunDetail/);
|
expect(agentDetailViewContent).toMatch(/fetchAgentRunDetail/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has startAgentRun import for Run Heartbeat button", () => {
|
it("has startAgentRun import for Run Now button", () => {
|
||||||
expect(agentDetailViewContent).toMatch(/import.*startAgentRun.*from.*api/);
|
expect(agentDetailViewContent).toMatch(/import.*startAgentRun.*from.*api/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has Run Heartbeat button in runs tab", () => {
|
it("has Run Now button in runs tab", () => {
|
||||||
expect(agentDetailViewContent).toMatch(/Run Heartbeat/);
|
expect(agentDetailViewContent).toMatch(/Run Now/);
|
||||||
expect(agentDetailViewContent).toMatch(/handleRunHeartbeat/);
|
expect(agentDetailViewContent).toMatch(/handleRunHeartbeat/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1130,9 +1130,9 @@ function RunsTab({
|
|||||||
<button
|
<button
|
||||||
className="btn btn--sm btn--primary"
|
className="btn btn--sm btn--primary"
|
||||||
onClick={() => void handleRunHeartbeat()}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1405,9 +1405,9 @@ function RunsTab({
|
|||||||
<button
|
<button
|
||||||
className="btn btn--sm btn--primary"
|
className="btn btn--sm btn--primary"
|
||||||
onClick={() => void handleRunHeartbeat()}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -730,16 +730,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
)}
|
)}
|
||||||
{agent.state === "active" && (
|
{agent.state === "active" && (
|
||||||
<>
|
<>
|
||||||
{agent.taskId && (
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
title="Run Now"
|
||||||
title="Run Heartbeat"
|
aria-label={`Run now for ${agent.name}`}
|
||||||
aria-label={`Run heartbeat for ${agent.name}`}
|
>
|
||||||
>
|
<Activity size={14} />
|
||||||
<Activity size={14} />
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className="btn btn--sm"
|
className="btn btn--sm"
|
||||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||||
@@ -776,16 +774,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
)}
|
)}
|
||||||
{agent.state === "running" && (
|
{agent.state === "running" && (
|
||||||
<>
|
<>
|
||||||
{agent.taskId && (
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
disabled
|
||||||
disabled
|
title="Run in progress"
|
||||||
title="Run in progress"
|
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
||||||
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
>
|
||||||
>
|
<Activity size={14} />
|
||||||
<Activity size={14} />
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className="btn btn--sm"
|
className="btn btn--sm"
|
||||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||||
@@ -990,16 +986,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
)}
|
)}
|
||||||
{agent.state === "active" && (
|
{agent.state === "active" && (
|
||||||
<>
|
<>
|
||||||
{agent.taskId && (
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
title="Run Now"
|
||||||
title="Run Heartbeat"
|
aria-label={`Run now for ${agent.name}`}
|
||||||
aria-label={`Run heartbeat for ${agent.name}`}
|
>
|
||||||
>
|
<Activity size={14} /> Run Now
|
||||||
<Activity size={14} /> Run
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className="btn btn--sm"
|
className="btn btn--sm"
|
||||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||||
@@ -1036,16 +1030,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
)}
|
)}
|
||||||
{agent.state === "running" && (
|
{agent.state === "running" && (
|
||||||
<>
|
<>
|
||||||
{agent.taskId && (
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
disabled
|
||||||
disabled
|
title="Run in progress"
|
||||||
title="Run in progress"
|
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
||||||
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
>
|
||||||
>
|
<Activity size={14} /> Running
|
||||||
<Activity size={14} /> Running
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className="btn btn--sm"
|
className="btn btn--sm"
|
||||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||||
|
|||||||
@@ -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", () => {
|
describe("delete agent", () => {
|
||||||
it("shows Delete button for idle and terminated agents in default view", async () => {
|
it("shows Delete button for idle and terminated agents in default view", async () => {
|
||||||
render(<AgentsView addToast={mockAddToast} />);
|
render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
|||||||
Reference in New Issue
Block a user