feat(ui): My Workforce — one flat list of agents, and the "+" starts a mission
The sidebar showed Organization -> Company -> Team -> Agent. On this workspace that read "My Workspace -> General -> Everyone": three levels of placeholder wrapping five agents, with five orgs and three companies named "My Workspace", "General" and "Workplace" between them. None of it was load-bearing in the UI. `agents` has no org/company/team column at all — membership is only the `team_members` join, which the mission executor uses to map graph nodes to claws — and /orgs, /companies and /teams already redirect to the dashboard. The tree survived in exactly one place. So the tree is now a single "My Workforce" root with the agents directly under it, expanded by default: a workforce collapsed behind a disclosure is one the user has to discover they own. The World tier keeps the full forest, because that visualisation is ABOUT structure and flattening it would remove its subject. Nothing is deleted — the group pages and their APIs are untouched. Both "+" affordances now open the MISSION wizard. They opened the deploy wizard, while the copy beside them said "deploy wizard" and the tooltip said "Deploy a new agent" — none of which is what someone arriving at an empty workspace wants to do first. You get a workforce BY running missions. Hand-staffing one is a real thing to want, just not the first thing, so it is demoted to "or create an agent yourself" rather than removed. "Add a new agent, team, company, or organization" becomes "Create your agent workforce". Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dd80b69992
commit
895413509d
@@ -22,8 +22,9 @@ import { panelParsers, type DeviceSize } from "@/lib/url/panel-params";
|
|||||||
import { type FlowItem } from "./flow/TopologyFlow";
|
import { type FlowItem } from "./flow/TopologyFlow";
|
||||||
import { WorldCanvas } from "../world/WorldCanvas";
|
import { WorldCanvas } from "../world/WorldCanvas";
|
||||||
import { ObservePanel } from "../observe/ObservePanel";
|
import { ObservePanel } from "../observe/ObservePanel";
|
||||||
import { StructureTree, orgNode, type TreeItem } from "./StructureTree";
|
import { StructureTree, orgNode, type TreeItem, clawNode } from "./StructureTree";
|
||||||
import { MissionsList } from "./MissionsList";
|
import { MissionsList } from "./MissionsList";
|
||||||
|
import { MissionWizard } from "./MissionWizard";
|
||||||
import { LevelUpInbox } from "./LevelUpInbox";
|
import { LevelUpInbox } from "./LevelUpInbox";
|
||||||
import { MissionCanvas } from "./MissionCanvas";
|
import { MissionCanvas } from "./MissionCanvas";
|
||||||
import { RepoList } from "./RepoList";
|
import { RepoList } from "./RepoList";
|
||||||
@@ -78,6 +79,7 @@ const mono = "'JetBrains Mono', ui-monospace, monospace";
|
|||||||
// workspace still has something to render. These ids aren't UUIDs and
|
// workspace still has something to render. These ids aren't UUIDs and
|
||||||
// don't exist in the DB — treat them as non-selectable for reap.
|
// don't exist in the DB — treat them as non-selectable for reap.
|
||||||
const SYNTHETIC_TREE_IDS = new Set([
|
const SYNTHETIC_TREE_IDS = new Set([
|
||||||
|
"my-workforce",
|
||||||
"my-workspace",
|
"my-workspace",
|
||||||
"ws-teams",
|
"ws-teams",
|
||||||
"ungrouped-co",
|
"ungrouped-co",
|
||||||
@@ -500,6 +502,14 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
|
|
||||||
const [teamDrawer, setTeamDrawer] = useState(false);
|
const [teamDrawer, setTeamDrawer] = useState(false);
|
||||||
const [deployOpen, setDeployOpen] = useState(false);
|
const [deployOpen, setDeployOpen] = useState(false);
|
||||||
|
// The "+" starts a MISSION, not an agent.
|
||||||
|
//
|
||||||
|
// Both "+" affordances used to open the deploy wizard — while the copy beside
|
||||||
|
// them said "deploy wizard" and the rail's tooltip said "Deploy a new agent",
|
||||||
|
// none of which is what someone arriving here wants to do first. You get a
|
||||||
|
// workforce BY running missions; hand-staffing one is the advanced path and
|
||||||
|
// keeps its own entry point below.
|
||||||
|
const [missionWizardOpen, setMissionWizardOpen] = useState(false);
|
||||||
// Brain registry rail (claw page) — collapsed by default, toggled by the brain
|
// Brain registry rail (claw page) — collapsed by default, toggled by the brain
|
||||||
// icon in the Agents sidebar header.
|
// icon in the Agents sidebar header.
|
||||||
const [registryOpen, setRegistryOpen] = useState(false);
|
const [registryOpen, setRegistryOpen] = useState(false);
|
||||||
@@ -557,13 +567,29 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
return walk(roots);
|
return walk(roots);
|
||||||
};
|
};
|
||||||
const worldCanvasRoots: TreeItem[] = narrowRoots(stripSynthetics(worldRoots), worldSel);
|
const worldCanvasRoots: TreeItem[] = narrowRoots(stripSynthetics(worldRoots), worldSel);
|
||||||
// Both the World and Agents tiers now share the same collapsible org →
|
// One flat root: "My Workforce", and the agents directly under it.
|
||||||
// company → team → agent tree — a single pane onto the whole workforce.
|
//
|
||||||
// `selectLevel` below still constrains selection to agents on the Agents
|
// The tree used to be Organization → Company → Team → Agent, and on a real
|
||||||
// tier so bulk-delete stays claw-scoped.
|
// workspace that read "My Workspace → General → Everyone" — three levels of
|
||||||
const treeRoots: TreeItem[] = worldRoots;
|
// placeholder wrapping five agents. None of it is load-bearing in the UI:
|
||||||
|
// `agents` has no org/company/team column at all (membership is only the
|
||||||
|
// `team_members` join, which the mission executor uses to map graph nodes to
|
||||||
|
// claws), and the /orgs, /companies and /teams pages already redirect here.
|
||||||
|
//
|
||||||
|
// The World tier keeps the full forest — that visualisation is ABOUT
|
||||||
|
// structure, so flattening it would remove its subject.
|
||||||
|
const workforceRoot: TreeItem = {
|
||||||
|
id: "my-workforce",
|
||||||
|
level: "org",
|
||||||
|
label: "My Workforce",
|
||||||
|
meta: `${allAgents.length} agent${allAgents.length === 1 ? "" : "s"}`,
|
||||||
|
children: allAgents.map(clawNode),
|
||||||
|
};
|
||||||
|
const treeRoots: TreeItem[] = isWorld ? worldRoots : [workforceRoot];
|
||||||
const treeActiveId = isClaw ? agentId : worldSel;
|
const treeActiveId = isClaw ? agentId : worldSel;
|
||||||
const treeAutoExpand: string[] = [];
|
// Open by default. A workforce collapsed behind one disclosure is a workforce
|
||||||
|
// the user has to discover they own.
|
||||||
|
const treeAutoExpand: string[] = isWorld ? [] : ["my-workforce"];
|
||||||
|
|
||||||
// Every node in the active tree (flattened) — the wrench multi-select looks up
|
// Every node in the active tree (flattened) — the wrench multi-select looks up
|
||||||
// selected nodes here regardless of depth, and derives the delete kind from the
|
// selected nodes here regardless of depth, and derives the delete kind from the
|
||||||
@@ -639,7 +665,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div style={{ width: 28, height: 1, background: "rgba(255,255,255,.08)", margin: "6px 0" }} />
|
<div style={{ width: 28, height: 1, background: "rgba(255,255,255,.08)", margin: "6px 0" }} />
|
||||||
<button type="button" onClick={() => setDeployOpen(true)} title="Deploy a new agent" style={{ width: 36, height: 36, borderRadius: 9, border: "1px dashed rgba(255,111,97,.4)", background: "rgba(255,111,97,.06)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff6f61", fontSize: 19, fontWeight: 300, cursor: "pointer" }}>+</button>
|
<button type="button" onClick={() => setMissionWizardOpen(true)} title="New mission" style={{ width: 36, height: 36, borderRadius: 9, border: "1px dashed rgba(255,111,97,.4)", background: "rgba(255,111,97,.06)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff6f61", fontSize: 19, fontWeight: 300, cursor: "pointer" }}>+</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -750,7 +776,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "24px 20px", gap: 10, textAlign: "center" }}>
|
<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={{ 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={{ 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>
|
<span style={{ fontFamily: mono, fontSize: 11, color: "#8a8a92", lineHeight: 1.6, maxWidth: 200 }}>Hit the <span style={{ color: "#ff8a7a" }}>+</span> to start a mission — the agents it needs join your workforce.</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -1004,7 +1030,10 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyRosterStage onAdd={() => setDeployOpen(true)} />
|
<EmptyRosterStage
|
||||||
|
onAdd={() => setMissionWizardOpen(true)}
|
||||||
|
onCreateAgent={() => setDeployOpen(true)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1014,6 +1043,18 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
{/* MASTER PLANNER — the "+" opens a chat with Opus 4.8 that proposes and
|
{/* MASTER PLANNER — the "+" opens a chat with Opus 4.8 that proposes and
|
||||||
scaffolds a whole team of agents. */}
|
scaffolds a whole team of agents. */}
|
||||||
{deployOpen ? <MasterPlannerModal onClose={() => setDeployOpen(false)} /> : null}
|
{deployOpen ? <MasterPlannerModal onClose={() => setDeployOpen(false)} /> : null}
|
||||||
|
|
||||||
|
{/* The mission wizard, behind every "+" on the dashboard. */}
|
||||||
|
{missionWizardOpen ? (
|
||||||
|
<MissionWizard
|
||||||
|
onClose={() => setMissionWizardOpen(false)}
|
||||||
|
onCreated={() => {
|
||||||
|
setMissionWizardOpen(false);
|
||||||
|
setTier("missions");
|
||||||
|
router.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{orphanDialogOpen ? (
|
{orphanDialogOpen ? (
|
||||||
<OrphanMigrationDialog
|
<OrphanMigrationDialog
|
||||||
onClose={() => setOrphanDialogOpen(false)}
|
onClose={() => setOrphanDialogOpen(false)}
|
||||||
@@ -1085,23 +1126,33 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Canvas empty state for a workspace with no orgs/companies/teams/agents.
|
/** Canvas empty state for a workspace with no agents yet.
|
||||||
* The + button opens the same MasterPlannerModal the rail's + does. */
|
*
|
||||||
function EmptyRosterStage({ onAdd }: { onAdd: () => void }) {
|
* The primary action starts a MISSION, because that is how a workforce comes
|
||||||
|
* to exist — a mission mints the agents it needs and they stay. Hand-staffing
|
||||||
|
* one is the advanced path and gets a quieter secondary link. */
|
||||||
|
function EmptyRosterStage({
|
||||||
|
onAdd,
|
||||||
|
onCreateAgent,
|
||||||
|
}: {
|
||||||
|
onAdd: () => void;
|
||||||
|
onCreateAgent: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<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%)" }}>
|
<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%)" }}>
|
||||||
<div style={{ fontFamily: mono, fontSize: 11, letterSpacing: ".18em", color: "#5a5a62" }}>YOUR WORKFORCE IS EMPTY</div>
|
<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 }}>
|
<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
|
Create your agent workforce
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontFamily: mono, fontSize: 12, color: "#8a8a92", maxWidth: 460, textAlign: "center", lineHeight: 1.6 }}>
|
<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.
|
Start a mission and the agents it needs are hired for it — they stay in
|
||||||
|
your workforce afterwards.
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAdd}
|
onClick={onAdd}
|
||||||
aria-label="Deploy a new agent"
|
aria-label="Start a mission"
|
||||||
title="Deploy — opens the same wizard as the rail's +"
|
title="Start a mission"
|
||||||
style={{
|
style={{
|
||||||
width: 74,
|
width: 74,
|
||||||
height: 74,
|
height: 74,
|
||||||
@@ -1123,6 +1174,25 @@ function EmptyRosterStage({ onAdd }: { onAdd: () => void }) {
|
|||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
|
{/* The old primary action, demoted rather than removed: staffing and
|
||||||
|
upskilling a workforce by hand is a real thing to want, just not the
|
||||||
|
first thing. */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCreateAgent}
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
border: 0,
|
||||||
|
color: "#8a8a92",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 11.5,
|
||||||
|
cursor: "pointer",
|
||||||
|
textDecoration: "underline",
|
||||||
|
textUnderlineOffset: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
or create an agent yourself
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user