master planner: topology info panel under the gallery

Selecting a card in the strip expands into an info panel below with:
- Coordinator badge (green when the topology has a lead, gray when
  leaderless — mirrors what cm-orchestrator's planner reads off the graph)
- Communication pattern one-liner
- 'When to use' guidance
- Auto-staffed role distribution chips (from the catalog's
  role_distribution — same numbers TeamWizard apportions)
- ASCII sketch of the shape

Cheat sheet is client-side (TOPO_DETAIL) so the expansion is instant on
selection — mirrors the exec-plan semantics baked into cm-orchestrator.
This commit is contained in:
Omar Sobh
2026-07-07 04:21:32 -07:00
parent ab4a62ef7d
commit 5e49086af7
@@ -91,6 +91,87 @@ type CatalogEntry = {
role_distribution: { role: string; weight: number }[]; role_distribution: { role: string; weight: number }[];
}; };
// Per-topology comm/coordinator/usage cheat sheet. Kept in the client so the
// info panel can render immediately after a selection without a second fetch;
// mirrors the semantics baked into cm-orchestrator's plan_steps().
const TOPO_DETAIL: Record<
string,
{ coordinator: string | null; comm: string; whenToUse: string; diagram: string }
> = {
hierarchical: {
coordinator: "root",
comm: "Root plans → children work in parallel with the plan as context → root synthesizes their outputs.",
whenToUse: "Decompose a goal, fan out, then produce one coherent answer.",
diagram: " root\n / | \\\n c1 c2 c3\n \\ | /\n root(synth)",
},
hub_spoke: {
coordinator: "hub",
comm: "Hub delegates a shard of the task to each spoke, then aggregates their replies.",
whenToUse: "One coordinator briefing specialists and collecting results.",
diagram: " hub\n / | | \\\n s1 s2 s3 s4\n \\ | | /\n hub",
},
star_moe: {
coordinator: "router",
comm: "Router classifies each subtask and dispatches it to the expert whose specialty fits.",
whenToUse: "Mixed-domain tasks where routing beats broadcasting.",
diagram: " router\n /|\\ \\\n e1 e2 e3 e4",
},
market: {
coordinator: "auctioneer",
comm: "Auctioneer posts tasks; agents bid on fit/cost; the winner works.",
whenToUse: "When you want cost/quality trade-offs surfaced automatically.",
diagram: " auctioneer\n | task\n a1 a2 a3\n (bids)",
},
pipeline: {
coordinator: null,
comm: "Linear stages: stage N's output is stage N+1's only input.",
whenToUse: "Multi-step transforms — draft → critique → polish.",
diagram: "s1 → s2 → s3 → s4",
},
ring: {
coordinator: null,
comm: "Cyclic — each pass hands the current draft to the next node, refining each lap.",
whenToUse: "Iterative polish where each agent adds a different pass.",
diagram: "a1 → a2\n ↑ ↓\na4 ← a3",
},
swarm: {
coordinator: null,
comm: "Many parallel workers attack the goal; a synth step aggregates their outputs into consensus.",
whenToUse: "Breadth-first exploration; parallelism > coordination.",
diagram: "w1 w2 w3 … wN\n \\ | | /\n synth",
},
flat: {
coordinator: null,
comm: "Independent peers work in parallel; nobody routes, nobody coordinates.",
whenToUse: "Trivially parallel tasks with no cross-dependencies.",
diagram: "a1 a2 a3 a4",
},
holacratic: {
coordinator: null,
comm: "Roles self-organize into circles; whichever role holds the ball works.",
whenToUse: "Dynamic responsibilities where the lead can shift per subtask.",
diagram: "( circle 1 )\n( circle 2 )\n( shared roles )",
},
mesh: {
coordinator: null,
comm: "Dense peer-to-peer exchange until the network converges on an answer.",
whenToUse: "Consensus problems — every agent's view weighs on every other.",
diagram: "a1—a2\n | X |\na3—a4",
},
blackboard: {
coordinator: null,
comm: "Shared workspace agents read/write freely; no fixed protocol between them.",
whenToUse: "Long-lived shared state, opportunistic contributors.",
diagram: "a1 a2 a3\n \\ | /\n [ blackboard ]",
},
debate: {
coordinator: "judge",
comm: "Proposer and critic argue in rounds; a judge decides at the end.",
whenToUse: "Adversarial verification — force each claim to survive a challenger.",
diagram: "proposer ⇄ critic\n ↓\n judge",
},
};
export function MasterPlannerModal({ onClose }: { onClose: () => void }) { export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
const router = useRouter(); const router = useRouter();
const [mode, setMode] = useState<Mode>("specialists"); const [mode, setMode] = useState<Mode>("specialists");
@@ -481,6 +562,100 @@ function TopologyStrip({
); );
})} })}
</div> </div>
{selected ? (
<TopologyInfoPanel
entry={catalog.find((c) => c.kind === selected)}
detail={TOPO_DETAIL[selected]}
/>
) : null}
</div>
);
}
function TopologyInfoPanel({
entry,
detail,
}: {
entry: CatalogEntry | undefined;
detail: { coordinator: string | null; comm: string; whenToUse: string; diagram: string } | undefined;
}) {
if (!entry) return null;
return (
<div
style={{
marginTop: 8,
display: "grid",
gridTemplateColumns: "minmax(0, 1fr) 168px",
gap: 12,
padding: "10px 12px",
borderRadius: 10,
border: "1px solid rgba(201,138,240,.22)",
background: "rgba(201,138,240,.05)",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
<div style={{ fontSize: 13, fontWeight: 700, color: "#f3f3f5" }}>{entry.name}</div>
<span
style={{
fontFamily: mono,
fontSize: 9.5,
padding: "2px 7px",
borderRadius: 5,
background: detail?.coordinator ? "rgba(127,208,160,.14)" : "rgba(255,255,255,.06)",
color: detail?.coordinator ? "#7fd0a0" : "#8a8a92",
}}
>
{detail?.coordinator ? `coordinator: ${detail.coordinator}` : "leaderless"}
</span>
</div>
{detail ? (
<div style={{ fontSize: 11.5, color: "#cfcfd5", lineHeight: 1.5 }}>{detail.comm}</div>
) : (
<div style={{ fontSize: 11.5, color: "#cfcfd5", lineHeight: 1.5 }}>{entry.description}</div>
)}
{detail ? (
<div style={{ fontFamily: mono, fontSize: 10.5, color: "#c98af0" }}>
when to use — <span style={{ color: "#eaeaee" }}>{detail.whenToUse}</span>
</div>
) : null}
{entry.role_distribution.length ? (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 2 }}>
{entry.role_distribution.map((r) => (
<span
key={r.role}
style={{
fontFamily: mono,
fontSize: 10,
padding: "2px 6px",
borderRadius: 5,
background: "rgba(255,255,255,.05)",
color: "#cfcfd5",
}}
>
{r.role} · {Math.round(r.weight * 100)}%
</span>
))}
</div>
) : null}
</div>
<pre
style={{
margin: 0,
padding: 8,
borderRadius: 6,
background: "#0a0a0d",
border: "1px solid rgba(255,255,255,.06)",
color: "#c98af0",
fontFamily: mono,
fontSize: 10.5,
lineHeight: 1.35,
whiteSpace: "pre",
overflow: "hidden",
}}
>
{detail?.diagram ?? "—"}
</pre>
</div> </div>
); );
} }