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:
Fusion
2026-05-05 02:50:35 -07:00
committed by gsxdsm
parent 17b7502c67
commit bca3854d30
4 changed files with 184 additions and 95 deletions

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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 () => {