agents empty state: kill synthetic fallback + real CTA
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 35s
ci / rust (push) Successful in 2m38s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 36s

After reaping every real agent/team/company/org, dashboard-data.ts
was still pushing a fabricated "My Workspace → Direct → Ungrouped
(0 claws)" org so the sidebar always had *something*. It was
confusing after a full cascade-reap ("didn't we just delete
everything?") and the placeholder nodes couldn't be selected/deleted
either.

- dashboard-data.ts: drop the empty-workspace fallback (myClawsOrg
  is no longer referenced; removed). Real emptiness now returns
  { orgs: [] }.
- Dashboard.tsx: when orgs.length === 0, replace the sidebar tree
  with a friendly "NOTHING TO SHOW / Your workforce is empty / Hit
  the + …" panel. Canvas gets a purposeful EmptyRosterStage with
  a big + button that opens the same MasterPlannerModal the rail
  already uses — one wizard, two entry points.
- Removed the old EmptyStage helper (no longer used).
This commit is contained in:
Omar Sobh
2026-07-08 14:06:24 -07:00
parent 58ed8a5948
commit 0cbde3a3cf
2 changed files with 55 additions and 22 deletions
@@ -665,6 +665,17 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<InfraNav view={infraSel ?? "local"} onSelect={setInfraSel} onConnectHost={() => setInfraConnectOpen(true)} /> <InfraNav view={infraSel ?? "local"} onSelect={setInfraSel} onConnectHost={() => setInfraConnectOpen(true)} />
) : ( ) : (
<> <>
{/* Empty roster — no real orgs/companies/teams/agents. Rather than
render an empty tree (or fake ones), point the user at the same
"+" deploy wizard the rail exposes. */}
{orgs.length === 0 ? (
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "24px 20px", gap: 10, textAlign: "center" }}>
<span style={{ fontFamily: mono, fontSize: 10.5, letterSpacing: ".14em", color: "#5a5a62" }}>NOTHING TO SHOW</span>
<span style={{ fontSize: 13, color: "#cfcfd5", lineHeight: 1.55 }}>Your workforce is empty.</span>
<span style={{ fontFamily: mono, fontSize: 11, color: "#8a8a92", lineHeight: 1.6, maxWidth: 200 }}>Hit the <span style={{ color: "#ff8a7a" }}>+</span> on the rail (or in the canvas) to add a new agent, team, company, or organization.</span>
</div>
) : (
<>
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */} {/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => { <StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => {
// Synthetic UI-only containers ("my-workspace", "ws-teams", // Synthetic UI-only containers ("my-workspace", "ws-teams",
@@ -682,6 +693,8 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
</div> </div>
</div> </div>
) : null} ) : null}
</>
)}
</> </>
)} )}
</div> </div>
@@ -885,7 +898,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
</div> </div>
</> </>
) : ( ) : (
<EmptyStage label="No agents yet — hit + to deploy your first agent." /> <EmptyRosterStage onAdd={() => setDeployOpen(true)} />
)} )}
</div> </div>
@@ -953,10 +966,44 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
); );
} }
function EmptyStage({ label }: { label: string }) { /** Canvas empty state for a workspace with no orgs/companies/teams/agents.
* The + button opens the same MasterPlannerModal the rail's + does. */
function EmptyRosterStage({ onAdd }: { onAdd: () => void }) {
return ( return (
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%)" }}> <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22, background: "radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%)" }}>
<span style={{ fontFamily: mono, fontSize: 12, color: "#5a5a62" }}>{label}</span> <div style={{ fontFamily: mono, fontSize: 11, letterSpacing: ".18em", color: "#5a5a62" }}>YOUR WORKFORCE IS EMPTY</div>
<div style={{ fontSize: 22, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.015em", maxWidth: 480, textAlign: "center", lineHeight: 1.25 }}>
Add a new agent, team, company, or organization
</div>
<div style={{ fontFamily: mono, fontSize: 12, color: "#8a8a92", maxWidth: 460, textAlign: "center", lineHeight: 1.6 }}>
The <span style={{ color: "#ff8a7a" }}>+</span> below opens the deploy wizard — pitch it a team spec and it scaffolds a whole workforce in one pass.
</div>
<button
type="button"
onClick={onAdd}
aria-label="Deploy a new agent"
title="Deploy — opens the same wizard as the rail's +"
style={{
width: 74,
height: 74,
borderRadius: 20,
border: "1px dashed rgba(255,111,97,.5)",
background: "rgba(255,111,97,.09)",
color: "#ff6f61",
fontSize: 34,
fontWeight: 200,
lineHeight: 1,
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
transition: "transform .18s ease, background .18s ease, border-color .18s ease",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,111,97,.16)"; e.currentTarget.style.borderColor = "rgba(255,111,97,.8)"; e.currentTarget.style.transform = "scale(1.04)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(255,111,97,.09)"; e.currentTarget.style.borderColor = "rgba(255,111,97,.5)"; e.currentTarget.style.transform = "scale(1)"; }}
>
+
</button>
</div> </div>
); );
} }
+4 -18
View File
@@ -89,22 +89,6 @@ async function buildOrg(id: string, name: string, clawById: Map<string, Agent>,
return { id, name, topology: node?.kind || "federated", companies }; return { id, name, topology: node?.kind || "federated", companies };
} }
function myClawsOrg(standalone: DemoAgent[]): DemoOrg {
// Holding area for claws not yet in a real team — NOT a dummy team: it's
// explicitly "Ungrouped" and empties out as claws are added to real teams
// (via "Add to teams"). Once every claw is in a real team, this disappears.
const running = standalone.some((c) => c.status === "running");
const team: DemoTeam = {
id: "ungrouped-team", name: "Ungrouped", topology: "flat",
status: running ? "running" : standalone.some((c) => c.status === "online") ? "online" : "idle",
dot: "#3a3a40", agents: standalone, groupApps: [],
};
return {
id: "my-workspace", name: "My Workspace", topology: "flat",
companies: [{ id: "ungrouped-co", name: "Direct", topology: "flat", meta: `${standalone.length} ungrouped claw${standalone.length === 1 ? "" : "s"}`, teams: [team] }],
};
}
/** Load the live workspace as the dashboard's nested view-model, plus the raw /** Load the live workspace as the dashboard's nested view-model, plus the raw
* claws (real `Agent`s) so the claw page can feed the computer + chat by id. * claws (real `Agent`s) so the claw page can feed the computer + chat by id.
* Always returns at least the "My Claws" group so created claws are visible. */ * Always returns at least the "My Claws" group so created claws are visible. */
@@ -162,8 +146,10 @@ export async function loadWorkspace(): Promise<{ orgs: DemoOrg[]; claws: Agent[]
} }
if (synthCompanies.length > 0) { if (synthCompanies.length > 0) {
populated.push({ id: "my-workspace", name: "My Workspace", topology: "flat", companies: synthCompanies }); populated.push({ id: "my-workspace", name: "My Workspace", topology: "flat", companies: synthCompanies });
} else if (populated.length === 0) {
populated.push(myClawsOrg([]));
} }
// Fully-empty workspaces used to fall back to a fake "My Workspace / Direct
// / 0 ungrouped" org — visually confusing after a full reap. Return an
// empty roots list instead so the shell can render a proper empty state
// that points at the "+" deploy wizard.
return { orgs: populated, claws }; return { orgs: populated, claws };
} }