feat(dashboard): unify agent lifecycle controls in AgentsView

- Remove Stop buttons from active/paused/running/error in both grid and list
  views; Pause/Resume/Retry cover the reversible transitions and Delete stays
  available on idle/terminated for teardown. Matches the existing shape of
  AgentDetailView after the earlier cleanup.
- Drop the redundant interval badge next to the heartbeat dropdown — the
  selected <option> already shows the current value.
- Add a "Next: HH:MM:SS" indicator alongside "Last:" for active/running
  agents, derived from lastHeartbeatAt + configuredIntervalMs (full timestamp
  in the tooltip). Paused/idle/error/terminated agents show Last only since
  there's no scheduled next tick.
- Remove now-unused Square and Heart icon imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-22 21:33:42 -07:00
parent d72153620b
commit 7e57ee48ef

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react"; import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api"; import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api"; import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api";
import { AgentDetailView } from "./AgentDetailView"; import { AgentDetailView } from "./AgentDetailView";
@@ -754,32 +754,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
> >
<Pause size={14} /> <Pause size={14} />
</button> </button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} />
</button>
</> </>
)} )}
{agent.state === "paused" && ( {agent.state === "paused" && (
<> <button
<button className="btn btn--sm"
className="btn btn--sm" onClick={() => void handleStateChange(agent.id, "active")}
onClick={() => void handleStateChange(agent.id, "active")} title="Resume"
title="Resume" >
> <Play size={14} />
<Play size={14} /> </button>
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} />
</button>
</>
)} )}
{agent.state === "running" && ( {agent.state === "running" && (
<> <>
@@ -798,32 +782,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
> >
<Pause size={14} /> <Pause size={14} />
</button> </button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} />
</button>
</> </>
)} )}
{agent.state === "error" && ( {agent.state === "error" && (
<> <button
<button className="btn btn--sm"
className="btn btn--sm" onClick={() => void handleStateChange(agent.id, "active")}
onClick={() => void handleStateChange(agent.id, "active")} title="Retry"
title="Retry" >
> <Play size={14} />
<Play size={14} /> </button>
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} />
</button>
</>
)} )}
{agent.state === "terminated" && ( {agent.state === "terminated" && (
<> <>
@@ -947,7 +915,6 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
)} )}
<div className="agent-heartbeat-control"> <div className="agent-heartbeat-control">
<span className="text-secondary">Heartbeat:</span> <span className="text-secondary">Heartbeat:</span>
<span className="badge text-secondary">{formatHeartbeatInterval(configuredIntervalMs)}</span>
<select <select
className="select agent-heartbeat-select" className="select agent-heartbeat-select"
value={configuredIntervalMs} value={configuredIntervalMs}
@@ -962,11 +929,23 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
))} ))}
</select> </select>
{isUpdatingHeartbeat && <span className="agent-heartbeat-saving text-secondary">Saving</span>} {isUpdatingHeartbeat && <span className="agent-heartbeat-saving text-secondary">Saving</span>}
{agent.lastHeartbeatAt && ( {agent.lastHeartbeatAt && (() => {
<span className="agent-heartbeat-last text-secondary" title={new Date(agent.lastHeartbeatAt).toLocaleString()}> const lastAt = new Date(agent.lastHeartbeatAt);
Last: {new Date(agent.lastHeartbeatAt).toLocaleTimeString()} const nextAt = new Date(lastAt.getTime() + configuredIntervalMs);
</span> const isTicking = agent.state === "active" || agent.state === "running";
)} return (
<>
<span className="agent-heartbeat-last text-secondary" title={lastAt.toLocaleString()}>
Last: {lastAt.toLocaleTimeString()}
</span>
{isTicking && (
<span className="agent-heartbeat-next text-secondary" title={nextAt.toLocaleString()}>
Next: {nextAt.toLocaleTimeString()}
</span>
)}
</>
);
})()}
</div> </div>
</div> </div>
@@ -1006,32 +985,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
> >
<Pause size={14} /> Pause <Pause size={14} /> Pause
</button> </button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} /> Stop
</button>
</> </>
)} )}
{agent.state === "paused" && ( {agent.state === "paused" && (
<> <button
<button className="btn btn--sm"
className="btn btn--sm" onClick={() => void handleStateChange(agent.id, "active")}
onClick={() => void handleStateChange(agent.id, "active")} title="Resume"
title="Resume" >
> <Play size={14} /> Resume
<Play size={14} /> Resume </button>
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} /> Stop
</button>
</>
)} )}
{agent.state === "running" && ( {agent.state === "running" && (
<> <>
@@ -1050,32 +1013,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
> >
<Pause size={14} /> Pause <Pause size={14} /> Pause
</button> </button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} /> Stop
</button>
</> </>
)} )}
{agent.state === "error" && ( {agent.state === "error" && (
<> <button
<button className="btn btn--sm"
className="btn btn--sm" onClick={() => void handleStateChange(agent.id, "active")}
onClick={() => void handleStateChange(agent.id, "active")} title="Retry"
title="Retry" >
> <Play size={14} /> Retry
<Play size={14} /> Retry </button>
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleStateChange(agent.id, "terminated")}
title="Stop"
>
<Square size={14} /> Stop
</button>
</>
)} )}
{agent.state === "terminated" && ( {agent.state === "terminated" && (
<> <>