feat(FN-3181): add initial runs tab with auto-expand selected run

Adds a "running control" to the agents dashboard that displays a live run entry and auto-expands to show active runs on load. The AgentsView and AgentDetailView components now support an initial runs tab with auto-expansion of the selected run, while the AgentDetailView gains an active-run context p

Fusion-Task-Id: FN-3181
This commit is contained in:
Fusion
2026-05-02 04:34:29 -07:00
committed by gsxdsm
parent 3ca4209757
commit f76a6755d9
5 changed files with 191 additions and 16 deletions

View File

@@ -62,6 +62,9 @@ interface AgentDetailViewProps {
addToast: (message: string, type?: "success" | "error") => void;
onChildClick?: (childId: string) => void;
inline?: boolean;
initialTab?: TabId;
initialRunId?: string | null;
preferActiveRun?: boolean;
}
type TabId = "dashboard" | "logs" | "config" | "runs" | "tasks" | "employees" | "soul" | "instructions" | "memory" | "reflections";
@@ -119,12 +122,12 @@ function pickDefaultAgentMemoryPath(files: MemoryFileInfo[], currentPath: string
?? "";
}
export function AgentDetailView({ agentId, projectId, onClose, addToast, onChildClick, inline = false }: AgentDetailViewProps) {
export function AgentDetailView({ agentId, projectId, onClose, addToast, onChildClick, inline = false, initialTab, initialRunId, preferActiveRun = false }: AgentDetailViewProps) {
const [agent, setAgent] = useState<AgentDetail | null>(null);
const { confirm } = useConfirm();
const [logs, setLogs] = useState<AgentLogEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState<TabId>("dashboard");
const [activeTab, setActiveTab] = useState<TabId>(initialTab ?? "dashboard");
const [isStreaming, setIsStreaming] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [latestRun, setLatestRun] = useState<AgentHeartbeatRun | null>(null);
@@ -636,6 +639,8 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
projectId={projectId}
agentState={agent.state}
agentName={agent.name}
initialRunId={initialRunId}
preferActiveRun={preferActiveRun}
/>
)}
@@ -1094,12 +1099,16 @@ function RunsTab({
projectId,
agentState,
agentName,
initialRunId,
preferActiveRun,
}: {
addToast: (msg: string, type?: "success" | "error") => void;
agentId: string;
projectId?: string;
agentState?: AgentState;
agentName?: string;
initialRunId?: string | null;
preferActiveRun?: boolean;
}) {
const [runs, setRuns] = useState<AgentHeartbeatRun[]>([]);
const { confirm } = useConfirm();
@@ -1109,6 +1118,7 @@ function RunsTab({
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
const [detailRun, setDetailRun] = useState<AgentHeartbeatRun | null>(null);
const [isLoadingDetail, setIsLoadingDetail] = useState(false);
const hasAutoExpandedInitialRunRef = useRef(false);
// Load runs on mount
const loadRuns = useCallback(async () => {
@@ -1194,6 +1204,25 @@ function RunsTab({
}
}, [selectedRunId, agentId, projectId, addToast]);
useEffect(() => {
hasAutoExpandedInitialRunRef.current = false;
}, [agentId, initialRunId, preferActiveRun]);
useEffect(() => {
if (runs.length === 0 || isLoadingRuns || hasAutoExpandedInitialRunRef.current) {
return;
}
const runToExpand = initialRunId
? runs.find((run) => run.id === initialRunId)
: (preferActiveRun ? runs.find((run) => run.status === "active") : null);
hasAutoExpandedInitialRunRef.current = true;
if (runToExpand) {
void handleRunClick(runToExpand.id);
}
}, [initialRunId, preferActiveRun, runs, isLoadingRuns, handleRunClick]);
const handleRunHeartbeat = async () => {
try {
await startAgentRun(agentId, projectId, { source: "on_demand", triggerDetail: "Triggered from dashboard" });

View File

@@ -188,6 +188,9 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
const [isImporting, setIsImporting] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const isMobileDetailOpen = isMobileViewport && !!selectedAgentId;
const [selectedAgentInitialTab, setSelectedAgentInitialTab] = useState<"dashboard" | "runs">("dashboard");
const [selectedAgentInitialRunId, setSelectedAgentInitialRunId] = useState<string | null>(null);
const [selectedAgentPreferActiveRun, setSelectedAgentPreferActiveRun] = useState(false);
const [agentView, setAgentView] = useState<"list" | "board" | "org">(() => {
if (typeof window === "undefined") return "list";
const saved = getScopedItem("fn-agent-view", projectId);
@@ -598,13 +601,23 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
}));
};
const openAgentDetail = useCallback((agentId: string, options?: { initialTab?: "dashboard" | "runs"; initialRunId?: string | null; preferActiveRun?: boolean }) => {
setSelectedAgentId(agentId);
setSelectedAgentInitialTab(options?.initialTab ?? "dashboard");
setSelectedAgentInitialRunId(options?.initialRunId ?? null);
setSelectedAgentPreferActiveRun(options?.preferActiveRun ?? false);
}, []);
const handleCloseDetail = useCallback(() => {
setSelectedAgentId(null);
setSelectedAgentInitialTab("dashboard");
setSelectedAgentInitialRunId(null);
setSelectedAgentPreferActiveRun(false);
}, []);
const handleChildClick = useCallback((childId: string) => {
setSelectedAgentId(childId);
}, []);
openAgentDetail(childId);
}, [openAgentDetail]);
const handleRunHeartbeat = async (agentId: string, agentName: string) => {
try {
@@ -878,7 +891,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<OrgChartNode
key={node.agent.id}
node={node}
onSelect={setSelectedAgentId}
onSelect={openAgentDetail}
getHealthStatus={getHealthStatus}
getRoleIcon={getRoleIcon}
getSkillBadges={getSkillBadges}
@@ -900,7 +913,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<div key={agent.id} className={`agent-board-card ${stateCardClass}${selectedAgentId === agent.id ? " agent-card--selected" : ""}`}>
<div
className="agent-board-clickable"
onClick={() => setSelectedAgentId(agent.id)}
onClick={() => openAgentDetail(agent.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
@@ -908,7 +921,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
if (e.key === " ") {
e.preventDefault();
}
setSelectedAgentId(agent.id);
openAgentDetail(agent.id);
}
}}
>
@@ -946,7 +959,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<div className="agent-card-header">
<div
className="agent-info agent-info--clickable"
onClick={() => setSelectedAgentId(agent.id)}
onClick={() => openAgentDetail(agent.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
@@ -954,7 +967,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
if (e.key === " ") {
e.preventDefault();
}
setSelectedAgentId(agent.id);
openAgentDetail(agent.id);
}
}}
>
@@ -1188,9 +1201,9 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<>
<button
className="btn btn--sm"
disabled
title="Run in progress"
aria-label={`Heartbeat run in progress for ${agent.name}`}
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
title="View live run details"
aria-label={`View live run details for ${agent.name}`}
>
<Activity size={14} /> Running
</button>
@@ -1226,7 +1239,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)}
<button
className="btn btn--sm agent-card-details-btn"
onClick={() => setSelectedAgentId(agent.id)}
onClick={() => openAgentDetail(agent.id)}
title={`View details for ${agent.name}`}
aria-label={`View details for ${agent.name}`}
>
@@ -1322,6 +1335,9 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
onClose={handleCloseDetail}
addToast={addToast}
onChildClick={handleChildClick}
initialTab={selectedAgentInitialTab}
initialRunId={selectedAgentInitialRunId}
preferActiveRun={selectedAgentPreferActiveRun}
/>
</Suspense>
) : (

View File

@@ -1119,6 +1119,87 @@ describe("AgentDetailView", () => {
});
});
it("opens directly to Runs tab and auto-expands the provided initial run", async () => {
const runId = "run-001";
mockFetchAgentRunLogs.mockResolvedValueOnce([
{
timestamp: "2024-01-01T00:00:00.000Z",
taskId: "agent-run",
text: "Run log line",
type: "text",
} as AgentLogEntry,
]);
mockFetchAgentRunDetail.mockResolvedValueOnce({
id: runId,
agentId: "agent-001",
startedAt: "2024-01-01T00:00:00.000Z",
endedAt: null,
status: "active",
systemPrompt: "System prompt text",
} as AgentHeartbeatRun);
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
initialTab="runs"
initialRunId={runId}
/>,
);
await waitFor(() => {
expect(mockFetchAgentRunLogs).toHaveBeenCalledWith("agent-001", runId, undefined);
expect(mockFetchAgentRunDetail).toHaveBeenCalledWith("agent-001", runId, undefined);
});
await waitFor(() => {
expect(screen.getByText("Run log line")).toBeInTheDocument();
expect(screen.getByText("System Prompt")).toBeInTheDocument();
});
});
it("auto-expands the active run when opened from running control context", async () => {
const activeRunId = "run-001";
mockFetchAgentRunLogs.mockResolvedValueOnce([
{
timestamp: "2024-01-01T00:00:00.000Z",
taskId: "agent-run",
text: "Active run log line",
type: "text",
} as AgentLogEntry,
]);
mockFetchAgentRunDetail.mockResolvedValueOnce({
id: activeRunId,
agentId: "agent-001",
startedAt: "2024-01-01T00:00:00.000Z",
endedAt: null,
status: "active",
systemPrompt: "Active run system prompt",
} as AgentHeartbeatRun);
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
initialTab="runs"
initialRunId={null}
preferActiveRun
/>,
);
await waitFor(() => {
expect(mockFetchAgentRunLogs).toHaveBeenCalledWith("agent-001", activeRunId, undefined);
expect(mockFetchAgentRunDetail).toHaveBeenCalledWith("agent-001", activeRunId, undefined);
});
await waitFor(() => {
expect(screen.getByText("Active run log line")).toBeInTheDocument();
expect(screen.getByText("System Prompt")).toBeInTheDocument();
});
});
describe("Logs tab", () => {
it("loads latest run logs lazily for agents without a current task", async () => {
const latestRun = {

View File

@@ -31,8 +31,10 @@ vi.mock("../../api", async (importOriginal) => {
});
vi.mock("../AgentDetailView", () => ({
AgentDetailView: ({ agentId, inline }: { agentId: string; inline?: boolean }) => (
<div data-testid="agent-detail-view" data-inline={inline ? "true" : "false"}>Agent detail: {agentId}</div>
AgentDetailView: ({ agentId, inline, initialTab, initialRunId, preferActiveRun }: { agentId: string; inline?: boolean; initialTab?: string; initialRunId?: string | null; preferActiveRun?: boolean }) => (
<div data-testid="agent-detail-view" data-inline={inline ? "true" : "false"} data-initial-tab={initialTab ?? "dashboard"} data-initial-run-id={initialRunId ?? ""} data-prefer-active-run={preferActiveRun ? "true" : "false"}>
Agent detail: {agentId}
</div>
),
relativeTime: () => "just now",
}));
@@ -591,7 +593,44 @@ describe("AgentsView", () => {
fireEvent.click(clickableIdentity!);
await waitFor(() => {
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("agent-001");
const detail = screen.getByTestId("agent-detail-view");
expect(detail).toHaveTextContent("agent-001");
expect(detail).toHaveAttribute("data-initial-tab", "dashboard");
expect(detail).toHaveAttribute("data-initial-run-id", "");
});
});
it("opens agent detail in Runs context when clicking Running control", async () => {
const runningAgent: Agent = {
id: "agent-005",
name: "Runner",
role: "executor",
state: "running",
activeRun: {
id: "run-555",
agentId: "agent-005",
startedAt: new Date().toISOString(),
endedAt: null,
status: "active",
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
};
mockFetchAgents.mockResolvedValue([runningAgent]);
mockFetchAgentStats.mockResolvedValue({ total: 1, byState: { running: 1 }, byRole: { executor: 1 } });
render(<AgentsView addToast={mockAddToast} />);
const runningButton = await screen.findByRole("button", { name: "View live run details for Runner" });
fireEvent.click(runningButton);
await waitFor(() => {
const detail = screen.getByTestId("agent-detail-view");
expect(detail).toHaveTextContent("agent-005");
expect(detail).toHaveAttribute("data-initial-tab", "runs");
expect(detail).toHaveAttribute("data-initial-run-id", "");
expect(detail).toHaveAttribute("data-prefer-active-run", "true");
});
});