feat(FN-3461): add org chart full-view mode with agent memory refresh
Merges two features: the org chart full-view mode (layout, controls, and documentation for FN-3461) and a QMD refresh fix that synchronizes the query memory database after agent dream writes, normalizing agent memory behavior. The AgentsView UI and agent-tools were updated alongside the new memory-b Fusion-Task-Id: FN-3461
This commit is contained in:
@@ -133,6 +133,28 @@
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.agents-org-full-view {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agents-view-content--org-full {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.agents-org-detail-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.agents-org-detail-back {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.agents-split-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(calc(var(--space-xl) * 11 + var(--space-xs)), calc(var(--space-xl) * 13 + var(--space-lg))) minmax(0, 1fr);
|
||||
@@ -1191,6 +1213,13 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agents-org-full-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agents-split-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -235,6 +235,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
const [onboardingDraft, setOnboardingDraft] = useState<AgentOnboardingSummary | null>(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [selectedOrgChartAgentId, setSelectedOrgChartAgentId] = useState<string | null>(null);
|
||||
const isMobileDetailOpen = isMobileViewport && !!selectedAgentId;
|
||||
const [selectedAgentInitialTab, setSelectedAgentInitialTab] = useState<"dashboard" | "runs">("dashboard");
|
||||
const [selectedAgentInitialRunId, setSelectedAgentInitialRunId] = useState<string | null>(null);
|
||||
@@ -669,6 +670,11 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
openAgentDetail(childId);
|
||||
}, [openAgentDetail]);
|
||||
|
||||
const handleOrgChartNodeSelect = useCallback((agentId: string) => {
|
||||
setSelectedOrgChartAgentId(agentId);
|
||||
openAgentDetail(agentId);
|
||||
}, [openAgentDetail]);
|
||||
|
||||
const handleOverviewAgentSelect = useCallback((agentId: string) => {
|
||||
openAgentDetail(agentId);
|
||||
if (isMobileViewport) {
|
||||
@@ -903,6 +909,36 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NewAgentDialog
|
||||
isOpen={isCreating}
|
||||
onClose={() => {
|
||||
setIsCreating(false);
|
||||
setOnboardingDraft(null);
|
||||
}}
|
||||
onCreated={() => { setIsCreating(false); setOnboardingDraft(null); void loadAgents(); }}
|
||||
projectId={projectId}
|
||||
prefillDraft={onboardingDraft}
|
||||
/>
|
||||
|
||||
<ExperimentalAgentOnboardingModal
|
||||
isOpen={isOnboardingOpen}
|
||||
onClose={() => setIsOnboardingOpen(false)}
|
||||
onUseDraft={(draft) => {
|
||||
setOnboardingDraft(draft);
|
||||
setIsOnboardingOpen(false);
|
||||
setIsCreating(true);
|
||||
}}
|
||||
projectId={projectId}
|
||||
existingAgents={agents}
|
||||
/>
|
||||
|
||||
<AgentImportModal
|
||||
isOpen={isImporting}
|
||||
onClose={() => setIsImporting(false)}
|
||||
onImported={() => void loadAgents()}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<AgentsOverviewBar
|
||||
stats={stats}
|
||||
activeAgents={displayActiveAgents}
|
||||
@@ -913,108 +949,116 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
onOpenTaskLogs={onOpenTaskLogs}
|
||||
/>
|
||||
|
||||
{agentView === "org" ? (
|
||||
<div className="agents-org-full-view">
|
||||
<div className="agents-view-content agents-view-content--org-full">
|
||||
{selectedAgentId ? (
|
||||
<div className="agents-org-detail-view" data-testid="agents-org-detail-view">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm agents-org-detail-back"
|
||||
onClick={handleCloseDetail}
|
||||
aria-label="Back to org chart"
|
||||
>
|
||||
Back to org chart
|
||||
</button>
|
||||
<Suspense fallback={null}>
|
||||
<AgentDetailView
|
||||
key={selectedAgentId}
|
||||
inline
|
||||
showInlineBackButton={false}
|
||||
agentId={selectedAgentId}
|
||||
projectId={projectId}
|
||||
onClose={handleCloseDetail}
|
||||
addToast={addToast}
|
||||
onChildClick={handleChildClick}
|
||||
initialTab={selectedAgentInitialTab}
|
||||
initialRunId={selectedAgentInitialRunId}
|
||||
preferActiveRun={selectedAgentPreferActiveRun}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
) : showInitialAgentsLoading ? (
|
||||
<div className="agents-view-loading" role="status" aria-live="polite">
|
||||
<RefreshCw size={18} className="spin" />
|
||||
<span>Loading agents...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-org-chart-shell" data-testid="agent-org-chart-shell">
|
||||
{isMobileViewport ? (
|
||||
<div className="agent-org-chart-controls" data-testid="agent-org-chart-controls">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon touch-target"
|
||||
onClick={() => setOrgChartZoomIndex((value) => Math.max(0, value - 1))}
|
||||
disabled={orgChartZoomIndex === 0}
|
||||
aria-label="Zoom out org chart"
|
||||
title="Zoom out"
|
||||
>
|
||||
<ZoomOut size={16} />
|
||||
</button>
|
||||
<span className="agent-org-chart-controls__zoom-label" aria-live="polite">{Math.round(orgChartZoom * 100)}%</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon touch-target"
|
||||
onClick={() => setOrgChartZoomIndex((value) => Math.min(ORG_CHART_ZOOM_LEVELS.length - 1, value + 1))}
|
||||
disabled={orgChartZoomIndex === ORG_CHART_ZOOM_LEVELS.length - 1}
|
||||
aria-label="Zoom in org chart"
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn touch-target btn-sm agent-org-chart-controls__fit-btn"
|
||||
onClick={() => setOrgChartZoomIndex(1)}
|
||||
aria-label="Fit org chart"
|
||||
title="Fit org chart"
|
||||
>
|
||||
<Minimize2 size={16} />
|
||||
Fit
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="agent-org-chart-viewport" data-testid="agent-org-chart-viewport">
|
||||
<div className={`agent-org-chart-canvas agent-org-chart-canvas--zoom-${Math.round(orgChartZoom * 100)}`}>
|
||||
<div className="agent-org-chart" data-testid="agent-org-chart">
|
||||
{isOrgTreeLoading ? (
|
||||
<div className="agent-org-chart__loading" role="status" aria-live="polite">
|
||||
<RefreshCw size={18} className="spin" />
|
||||
<span>Loading org chart...</span>
|
||||
</div>
|
||||
) : displayOrgTree.length === 0 ? (
|
||||
<AgentEmptyState onCtaClick={handleOpenNewAgent} />
|
||||
) : (
|
||||
displayOrgTree.map((node) => (
|
||||
<OrgChartNode
|
||||
key={node.agent.id}
|
||||
node={node}
|
||||
onSelect={handleOrgChartNodeSelect}
|
||||
getHealthStatus={getHealthStatus}
|
||||
getRoleIcon={getRoleIcon}
|
||||
selectedAgentId={selectedOrgChartAgentId}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agents-split-layout">
|
||||
<div className={`agents-split-sidebar${isMobileDetailOpen ? " agents-split-sidebar--hidden-mobile" : ""}`}>
|
||||
<div className="agents-view-content">
|
||||
<NewAgentDialog
|
||||
isOpen={isCreating}
|
||||
onClose={() => {
|
||||
setIsCreating(false);
|
||||
setOnboardingDraft(null);
|
||||
}}
|
||||
onCreated={() => { setIsCreating(false); setOnboardingDraft(null); void loadAgents(); }}
|
||||
projectId={projectId}
|
||||
prefillDraft={onboardingDraft}
|
||||
/>
|
||||
|
||||
<ExperimentalAgentOnboardingModal
|
||||
isOpen={isOnboardingOpen}
|
||||
onClose={() => setIsOnboardingOpen(false)}
|
||||
onUseDraft={(draft) => {
|
||||
setOnboardingDraft(draft);
|
||||
setIsOnboardingOpen(false);
|
||||
setIsCreating(true);
|
||||
}}
|
||||
projectId={projectId}
|
||||
existingAgents={agents}
|
||||
/>
|
||||
|
||||
<AgentImportModal
|
||||
isOpen={isImporting}
|
||||
onClose={() => setIsImporting(false)}
|
||||
onImported={() => void loadAgents()}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
{/* Agent Collection */}
|
||||
{showInitialAgentsLoading ? (
|
||||
<div className="agents-view-loading" role="status" aria-live="polite">
|
||||
<RefreshCw size={18} className="spin" />
|
||||
<span>Loading agents...</span>
|
||||
</div>
|
||||
) : agentView === "org" ? (
|
||||
<div className="agent-org-chart-shell" data-testid="agent-org-chart-shell">
|
||||
{isMobileViewport ? (
|
||||
<div className="agent-org-chart-controls" data-testid="agent-org-chart-controls">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon touch-target"
|
||||
onClick={() => setOrgChartZoomIndex((value) => Math.max(0, value - 1))}
|
||||
disabled={orgChartZoomIndex === 0}
|
||||
aria-label="Zoom out org chart"
|
||||
title="Zoom out"
|
||||
>
|
||||
<ZoomOut size={16} />
|
||||
</button>
|
||||
<span className="agent-org-chart-controls__zoom-label" aria-live="polite">{Math.round(orgChartZoom * 100)}%</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon touch-target"
|
||||
onClick={() => setOrgChartZoomIndex((value) => Math.min(ORG_CHART_ZOOM_LEVELS.length - 1, value + 1))}
|
||||
disabled={orgChartZoomIndex === ORG_CHART_ZOOM_LEVELS.length - 1}
|
||||
aria-label="Zoom in org chart"
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn touch-target btn-sm agent-org-chart-controls__fit-btn"
|
||||
onClick={() => setOrgChartZoomIndex(1)}
|
||||
aria-label="Fit org chart"
|
||||
title="Fit org chart"
|
||||
>
|
||||
<Minimize2 size={16} />
|
||||
Fit
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="agent-org-chart-viewport" data-testid="agent-org-chart-viewport">
|
||||
<div className={`agent-org-chart-canvas agent-org-chart-canvas--zoom-${Math.round(orgChartZoom * 100)}`}>
|
||||
<div className="agent-org-chart" data-testid="agent-org-chart">
|
||||
{isOrgTreeLoading ? (
|
||||
<div className="agent-org-chart__loading" role="status" aria-live="polite">
|
||||
<RefreshCw size={18} className="spin" />
|
||||
<span>Loading org chart...</span>
|
||||
</div>
|
||||
) : displayOrgTree.length === 0 ? (
|
||||
<AgentEmptyState onCtaClick={handleOpenNewAgent} />
|
||||
) : (
|
||||
displayOrgTree.map((node) => (
|
||||
<OrgChartNode
|
||||
key={node.agent.id}
|
||||
node={node}
|
||||
onSelect={openAgentDetail}
|
||||
getHealthStatus={getHealthStatus}
|
||||
getRoleIcon={getRoleIcon}
|
||||
selectedAgentId={selectedAgentId}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : agentView === "board" ? (
|
||||
<div className="agent-board">
|
||||
{displayAgents.length === 0 ? (
|
||||
@@ -1409,6 +1453,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,7 @@ describe("AgentsView", () => {
|
||||
|
||||
it("renders org chart nodes and opens detail view when clicking a node", async () => {
|
||||
mockFetchOrgTree.mockResolvedValue(orgTree);
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
|
||||
|
||||
@@ -1222,11 +1222,25 @@ describe("AgentsView", () => {
|
||||
expect(screen.getAllByText(/Healthy|Idle|Paused|Unresponsive|Agent stalled/).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(container.querySelector(".agents-split-layout")).toBeNull();
|
||||
expect(container.querySelector(".agents-org-full-view")).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByText("Director One"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-detail-view")).toHaveTextContent("agent-child-1");
|
||||
expect(screen.getByRole("button", { name: "Back to org chart" })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Back to agents" })).toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back to org chart" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("agent-detail-view")).toBeNull();
|
||||
expect(screen.getByTestId("agent-org-chart")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(container.querySelector("[class*='org-chart-node-card--'].agent-card--selected")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps org chart node metadata compact without skill badges", async () => {
|
||||
|
||||
Reference in New Issue
Block a user