master planner: pink brain, agent tab, mode-scoped topologies + svg illustrations
ci / gates (push) Successful in 11s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 3m6s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m33s

Six planner-modal fixes in one pass:

- Header icon swap: Sparkles → Brain, styled pink (rgba(255,105,180,*))
  to match the "brain" framing the user wanted.
- Drop the "Claude Opus 4.8 designs & deploys." subtitle — the title
  carries enough weight on its own.
- Rename the first mode tab "Specialists" → "Agent". This card is
  specifically for building a single specialist. Updated INTRO to
  ask for one agent (job title + system prompt), so the planner
  chat behaves accordingly.
- Team hint: "4–8 agents, balanced" → "4–6 agents, balanced" so the
  UI matches the intro text ("4–6 person growth team").
- Topology strip is now mode-scoped. Team mode surfaces org-shaped
  structures (hierarchical, hub_spoke, star_moe, pipeline, ring,
  holacratic, debate, flat); Swarm mode surfaces formation-shaped
  ones (swarm, mesh, blackboard, market, flat). `flat` bridges
  both because it serves either shape. The strip header now
  reads "Topology · N available for {mode}".
- Replaced the ASCII `<pre>` diagram with inline SVG illustrations
  per kind. Coordinator nodes render in the accent pink (#ff69b4);
  worker/peer nodes are neutral gray. Twelve illustrations total,
  one per topology kind, in a lookup map (`TOPO_ILLUSTRATION`).
  If we later want the exact clawbernetes.work marketing images,
  drop them into /public/topologies/<kind>.svg and swap the
  renderer to an <img/>.
This commit is contained in:
Omar Sobh
2026-07-08 15:01:40 -07:00
parent 0cbde3a3cf
commit f0a2bd8a86
@@ -7,20 +7,20 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Activity, CalendarClock, Send, Sparkles, Users, Webhook } from "lucide-react"; import { Activity, Brain, CalendarClock, Send, Users, Webhook } from "lucide-react";
const mono = "'JetBrains Mono', ui-monospace, monospace"; const mono = "'JetBrains Mono', ui-monospace, monospace";
type Mode = "specialists" | "team" | "swarm" | "scheduled" | "triggered"; type Mode = "specialists" | "team" | "swarm" | "scheduled" | "triggered";
const MODES: { key: Mode; label: string; hint: string }[] = [ const MODES: { key: Mode; label: string; hint: string }[] = [
{ key: "specialists", label: "Specialists", hint: "23 domain experts" }, { key: "specialists", label: "Agent", hint: "one specialist agent" },
{ key: "team", label: "Team", hint: "48 agents, balanced" }, { key: "team", label: "Team", hint: "46 agents, balanced" },
{ key: "swarm", label: "Swarm", hint: "10+ workers, self-verifying" }, { key: "swarm", label: "Swarm", hint: "10+ workers, self-verifying" },
{ key: "scheduled", label: "Scheduled", hint: "ephemeral · date/time" }, { key: "scheduled", label: "Scheduled", hint: "ephemeral · date/time" },
{ key: "triggered", label: "Triggered", hint: "ephemeral · webhook" }, { key: "triggered", label: "Triggered", hint: "ephemeral · webhook" },
]; ];
const INTRO: Record<Mode, string> = { const INTRO: Record<Mode, string> = {
specialists: "Describe what you need — e.g. “a group of 23 specialists for a React Native app: a Rust backend expert, a RN UI dev, and a release engineer.” I'll write high-quality prompts for each.", specialists: "Describe the single agent you need — e.g. “a Rust backend expert who knows Actix and Diesel deeply, focused on reviewing our API layer for correctness bugs.” I'll write a high-quality system prompt and job title for one agent.",
team: "Describe the goal — e.g. “build me a 46 person growth team: a coordinator, a researcher, a writer, an analyst, and a critic.” I'll pick roles that actually get exercised each iteration.", team: "Describe the goal — e.g. “build me a 46 person growth team: a coordinator, a researcher, a writer, an analyst, and a critic.” I'll pick roles that actually get exercised each iteration.",
swarm: "Describe a job to verify-loop across 10+ workers — e.g. “analyze 100 EV companies; every figure needs a resolvable source URL.” I'll turn it into a checklist the verifier enforces.", swarm: "Describe a job to verify-loop across 10+ workers — e.g. “analyze 100 EV companies; every figure needs a resolvable source URL.” I'll turn it into a checklist the verifier enforces.",
scheduled: "Describe a team and when it should run — e.g. “a market-news digest team, every weekday at 7am.” The team is spun up at fire time and torn down after.", scheduled: "Describe a team and when it should run — e.g. “a market-news digest team, every weekday at 7am.” The team is spun up at fire time and torn down after.",
@@ -91,6 +91,28 @@ type CatalogEntry = {
role_distribution: { role: string; weight: number }[]; role_distribution: { role: string; weight: number }[];
}; };
// Topologies split by mode. Teams read as organizational structures (a
// coordinator, a workflow, a role rotation); swarms read as formations —
// broad, parallel, self-organizing. `flat` lives in both because a flat
// spread of peers can serve either shape depending on the task.
const TEAM_TOPOLOGIES: readonly string[] = [
"hierarchical",
"hub_spoke",
"star_moe",
"pipeline",
"ring",
"holacratic",
"debate",
"flat",
];
const SWARM_TOPOLOGIES: readonly string[] = [
"swarm",
"mesh",
"blackboard",
"market",
"flat",
];
// Per-topology comm/coordinator/usage cheat sheet. Kept in the client so the // 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; // info panel can render immediately after a selection without a second fetch;
// mirrors the semantics baked into cm-orchestrator's plan_steps(). // mirrors the semantics baked into cm-orchestrator's plan_steps().
@@ -350,10 +372,9 @@ export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
<div onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Master Planner" style={{ width: "100%", maxWidth: 960, height: "86vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0d0d10", border: "1px solid rgba(255,255,255,.1)", boxShadow: "0 30px 90px rgba(0,0,0,.6)", overflow: "hidden", animation: "scale-in .18s ease" }}> <div onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Master Planner" style={{ width: "100%", maxWidth: 960, height: "86vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0d0d10", border: "1px solid rgba(255,255,255,.1)", boxShadow: "0 30px 90px rgba(0,0,0,.6)", overflow: "hidden", animation: "scale-in .18s ease" }}>
{/* Header + mode selector */} {/* Header + mode selector */}
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid rgba(255,255,255,.07)" }}> <div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid rgba(255,255,255,.07)" }}>
<span style={{ width: 32, height: 32, borderRadius: 9, background: "rgba(201,138,240,.14)", border: "1px solid rgba(201,138,240,.32)", display: "flex", alignItems: "center", justifyContent: "center", color: "#c98af0" }}><Sparkles size={16} /></span> <span style={{ width: 32, height: 32, borderRadius: 9, background: "rgba(255,105,180,.14)", border: "1px solid rgba(255,105,180,.36)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff69b4" }}><Brain size={16} /></span>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>Master Planner</div> <div style={{ fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>Master Planner</div>
<div style={{ fontSize: 11.5, color: "#8a8a92" }}>Claude Opus 4.8 designs &amp; deploys.</div>
</div> </div>
<div style={{ display: "flex", gap: 3, padding: 3, borderRadius: 9, background: "rgba(255,255,255,.04)", border: "1px solid rgba(255,255,255,.08)" }}> <div style={{ display: "flex", gap: 3, padding: 3, borderRadius: 9, background: "rgba(255,255,255,.04)", border: "1px solid rgba(255,255,255,.08)" }}>
{MODES.map((m) => ( {MODES.map((m) => (
@@ -365,6 +386,7 @@ export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
{(mode === "team" || mode === "swarm") ? ( {(mode === "team" || mode === "swarm") ? (
<TopologyStrip <TopologyStrip
mode={mode}
catalog={catalog} catalog={catalog}
selected={topologyKind} selected={topologyKind}
onSelect={setTopologyKind} onSelect={setTopologyKind}
@@ -462,14 +484,18 @@ export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
} }
function TopologyStrip({ function TopologyStrip({
mode,
catalog, catalog,
selected, selected,
onSelect, onSelect,
}: { }: {
mode: "team" | "swarm";
catalog: CatalogEntry[]; catalog: CatalogEntry[];
selected: string | null; selected: string | null;
onSelect: (kind: string | null) => void; onSelect: (kind: string | null) => void;
}) { }) {
const allowed = mode === "team" ? TEAM_TOPOLOGIES : SWARM_TOPOLOGIES;
const filtered = catalog.filter((c) => allowed.includes(c.kind));
if (catalog.length === 0) { if (catalog.length === 0) {
return ( return (
<div <div
@@ -508,7 +534,7 @@ function TopologyStrip({
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<span>Topology · {catalog.length} available</span> <span>Topology · {filtered.length} available for {mode}</span>
{selected ? ( {selected ? (
<button <button
type="button" type="button"
@@ -538,7 +564,7 @@ function TopologyStrip({
scrollbarWidth: "thin", scrollbarWidth: "thin",
}} }}
> >
{catalog.map((c) => { {filtered.map((c) => {
const active = selected === c.kind; const active = selected === c.kind;
return ( return (
<button <button
@@ -584,7 +610,7 @@ function TopologyStrip({
</div> </div>
{selected ? ( {selected ? (
<TopologyInfoPanel <TopologyInfoPanel
entry={catalog.find((c) => c.kind === selected)} entry={filtered.find((c) => c.kind === selected)}
detail={TOPO_DETAIL[selected]} detail={TOPO_DETAIL[selected]}
/> />
) : null} ) : null}
@@ -592,6 +618,203 @@ function TopologyStrip({
); );
} }
// ---------- Inline topology illustrations --------------------------------
// Small SVG glyphs, roughly 168×120. Coordinator role uses accent pink
// (#ff69b4); worker/peer nodes are neutral gray. If a project ever wants
// the exact clawbernetes.work marketing images, drop them into
// /public/topologies/<kind>.svg and replace this map with an <img/>.
const PINK = "#ff69b4";
const NODE = "#8a8a92";
const EDGE = "rgba(201,138,240,.55)";
const NODE_TEXT = "#eaeaee";
function Circle({ cx, cy, r, fill, stroke, label }: { cx: number; cy: number; r: number; fill: string; stroke?: string; label?: string }) {
return (
<g>
<circle cx={cx} cy={cy} r={r} fill={fill} stroke={stroke ?? "rgba(255,255,255,.14)"} strokeWidth="1" />
{label ? <text x={cx} y={cy + 3.5} textAnchor="middle" fontSize="8.5" fill={NODE_TEXT} fontFamily="ui-monospace, monospace">{label}</text> : null}
</g>
);
}
function Arrow({ x1, y1, x2, y2 }: { x1: number; y1: number; x2: number; y2: number }) {
return <line x1={x1} y1={y1} x2={x2} y2={y2} stroke={EDGE} strokeWidth="1.4" markerEnd="url(#tp-arrow)" />;
}
function Wire({ x1, y1, x2, y2 }: { x1: number; y1: number; x2: number; y2: number }) {
return <line x1={x1} y1={y1} x2={x2} y2={y2} stroke={EDGE} strokeWidth="1.4" />;
}
function TopoSvg({ children }: { children: React.ReactNode }) {
return (
<svg viewBox="0 0 168 120" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="tp-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0,0 L10,5 L0,10 z" fill={EDGE} />
</marker>
</defs>
{children}
</svg>
);
}
const TOPO_ILLUSTRATION: Record<string, React.FC> = {
hierarchical: () => (
<TopoSvg>
<Arrow x1={84} y1={22} x2={30} y2={62} />
<Arrow x1={84} y1={22} x2={84} y2={62} />
<Arrow x1={84} y1={22} x2={138} y2={62} />
<Arrow x1={30} y1={78} x2={84} y2={106} />
<Arrow x1={84} y1={78} x2={84} y2={106} />
<Arrow x1={138} y1={78} x2={84} y2={106} />
<Circle cx={84} cy={18} r={12} fill={PINK} label="R" />
<Circle cx={30} cy={70} r={10} fill={NODE} label="c1" />
<Circle cx={84} cy={70} r={10} fill={NODE} label="c2" />
<Circle cx={138} cy={70} r={10} fill={NODE} label="c3" />
<Circle cx={84} cy={106} r={10} fill={PINK} label="R" />
</TopoSvg>
),
hub_spoke: () => (
<TopoSvg>
<Wire x1={84} y1={60} x2={30} y2={24} />
<Wire x1={84} y1={60} x2={138} y2={24} />
<Wire x1={84} y1={60} x2={30} y2={96} />
<Wire x1={84} y1={60} x2={138} y2={96} />
<Circle cx={30} cy={24} r={10} fill={NODE} label="s1" />
<Circle cx={138} cy={24} r={10} fill={NODE} label="s2" />
<Circle cx={30} cy={96} r={10} fill={NODE} label="s3" />
<Circle cx={138} cy={96} r={10} fill={NODE} label="s4" />
<Circle cx={84} cy={60} r={14} fill={PINK} label="hub" />
</TopoSvg>
),
star_moe: () => (
<TopoSvg>
<Arrow x1={84} y1={30} x2={22} y2={92} />
<Arrow x1={84} y1={30} x2={62} y2={92} />
<Arrow x1={84} y1={30} x2={106} y2={92} />
<Arrow x1={84} y1={30} x2={146} y2={92} />
<Circle cx={84} cy={22} r={13} fill={PINK} label="rt" />
<Circle cx={22} cy={100} r={10} fill={NODE} label="e1" />
<Circle cx={62} cy={100} r={10} fill={NODE} label="e2" />
<Circle cx={106} cy={100} r={10} fill={NODE} label="e3" />
<Circle cx={146} cy={100} r={10} fill={NODE} label="e4" />
</TopoSvg>
),
pipeline: () => (
<TopoSvg>
<Arrow x1={30} y1={60} x2={64} y2={60} />
<Arrow x1={78} y1={60} x2={112} y2={60} />
<Arrow x1={126} y1={60} x2={148} y2={60} />
<Circle cx={22} cy={60} r={11} fill={NODE} label="s1" />
<Circle cx={72} cy={60} r={11} fill={NODE} label="s2" />
<Circle cx={122} cy={60} r={11} fill={NODE} label="s3" />
<Circle cx={156} cy={60} r={11} fill={PINK} label="✓" />
</TopoSvg>
),
ring: () => (
<TopoSvg>
<Arrow x1={84} y1={22} x2={132} y2={54} />
<Arrow x1={140} y1={70} x2={110} y2={102} />
<Arrow x1={70} y1={106} x2={38} y2={78} />
<Arrow x1={30} y1={62} x2={62} y2={30} />
<Circle cx={84} cy={18} r={11} fill={NODE} label="a1" />
<Circle cx={140} cy={60} r={11} fill={NODE} label="a2" />
<Circle cx={100} cy={106} r={11} fill={NODE} label="a3" />
<Circle cx={28} cy={60} r={11} fill={NODE} label="a4" />
</TopoSvg>
),
swarm: () => (
<TopoSvg>
{Array.from({ length: 7 }).map((_, i) => {
const x = 14 + i * 23;
return <Wire key={`w${i}`} x1={x} y1={38} x2={84} y2={92} />;
})}
{Array.from({ length: 7 }).map((_, i) => {
const x = 14 + i * 23;
return <Circle key={`c${i}`} cx={x} cy={30} r={8} fill={NODE} />;
})}
<Circle cx={84} cy={98} r={13} fill={PINK} label="∑" />
</TopoSvg>
),
flat: () => (
<TopoSvg>
<Circle cx={26} cy={60} r={11} fill={NODE} label="a1" />
<Circle cx={70} cy={60} r={11} fill={NODE} label="a2" />
<Circle cx={114} cy={60} r={11} fill={NODE} label="a3" />
<Circle cx={156} cy={60} r={11} fill={NODE} label="a4" />
</TopoSvg>
),
holacratic: () => (
<TopoSvg>
<circle cx={84} cy={60} r={50} fill="none" stroke={EDGE} strokeDasharray="3 4" strokeWidth="1.2" />
<circle cx={64} cy={54} r={26} fill="none" stroke={PINK} strokeWidth="1.4" />
<circle cx={106} cy={68} r={26} fill="none" stroke="#c98af0" strokeWidth="1.4" />
<Circle cx={60} cy={44} r={8} fill={NODE} />
<Circle cx={78} cy={62} r={8} fill={NODE} />
<Circle cx={104} cy={54} r={8} fill={NODE} />
<Circle cx={112} cy={80} r={8} fill={NODE} />
</TopoSvg>
),
mesh: () => (
<TopoSvg>
{(() => {
const p = [
[42, 26], [126, 26], [42, 94], [126, 94],
] as [number, number][];
const lines: React.ReactNode[] = [];
for (let i = 0; i < p.length; i++) {
for (let j = i + 1; j < p.length; j++) {
lines.push(<Wire key={`m${i}${j}`} x1={p[i][0]} y1={p[i][1]} x2={p[j][0]} y2={p[j][1]} />);
}
}
return lines;
})()}
<Circle cx={42} cy={26} r={11} fill={NODE} label="a1" />
<Circle cx={126} cy={26} r={11} fill={NODE} label="a2" />
<Circle cx={42} cy={94} r={11} fill={NODE} label="a3" />
<Circle cx={126} cy={94} r={11} fill={NODE} label="a4" />
</TopoSvg>
),
blackboard: () => (
<TopoSvg>
<rect x={38} y={48} width={92} height={30} rx={4} fill="rgba(201,138,240,.10)" stroke={PINK} strokeWidth="1.2" />
<text x={84} y={68} textAnchor="middle" fontSize="10" fill={PINK} fontFamily="ui-monospace, monospace">blackboard</text>
<Wire x1={30} y1={26} x2={60} y2={48} />
<Wire x1={84} y1={22} x2={84} y2={48} />
<Wire x1={138} y1={26} x2={110} y2={48} />
<Wire x1={30} y1={104} x2={60} y2={78} />
<Wire x1={138} y1={104} x2={110} y2={78} />
<Circle cx={26} cy={22} r={9} fill={NODE} />
<Circle cx={84} cy={16} r={9} fill={NODE} />
<Circle cx={142} cy={22} r={9} fill={NODE} />
<Circle cx={26} cy={104} r={9} fill={NODE} />
<Circle cx={142} cy={104} r={9} fill={NODE} />
</TopoSvg>
),
debate: () => (
<TopoSvg>
<Arrow x1={54} y1={38} x2={110} y2={38} />
<Arrow x1={110} y1={50} x2={54} y2={50} />
<Arrow x1={54} y1={54} x2={84} y2={90} />
<Arrow x1={110} y1={54} x2={84} y2={90} />
<Circle cx={40} cy={44} r={14} fill={NODE} label="prop" />
<Circle cx={124} cy={44} r={14} fill={NODE} label="crit" />
<Circle cx={84} cy={102} r={14} fill={PINK} label="judge" />
</TopoSvg>
),
market: () => (
<TopoSvg>
<Circle cx={84} cy={22} r={14} fill={PINK} label="auc" />
<Wire x1={84} y1={36} x2={30} y2={80} />
<Wire x1={84} y1={36} x2={84} y2={80} />
<Wire x1={84} y1={36} x2={138} y2={80} />
<text x={30} y={62} textAnchor="middle" fontSize="8" fill={EDGE} fontFamily="ui-monospace, monospace">bid</text>
<text x={84} y={62} textAnchor="middle" fontSize="8" fill={EDGE} fontFamily="ui-monospace, monospace">bid</text>
<text x={138} y={62} textAnchor="middle" fontSize="8" fill={EDGE} fontFamily="ui-monospace, monospace">bid</text>
<Circle cx={30} cy={94} r={11} fill={NODE} label="a1" />
<Circle cx={84} cy={94} r={11} fill={NODE} label="a2" />
<Circle cx={138} cy={94} r={11} fill={NODE} label="a3" />
</TopoSvg>
),
};
function TopologyInfoPanel({ function TopologyInfoPanel({
entry, entry,
detail, detail,
@@ -659,23 +882,25 @@ function TopologyInfoPanel({
</div> </div>
) : null} ) : null}
</div> </div>
<pre <div
style={{ style={{
margin: 0, padding: 6,
padding: 8, borderRadius: 8,
borderRadius: 6,
background: "#0a0a0d", background: "#0a0a0d",
border: "1px solid rgba(255,255,255,.06)", border: "1px solid rgba(255,255,255,.06)",
color: "#c98af0", display: "flex",
fontFamily: mono, alignItems: "center",
fontSize: 10.5, justifyContent: "center",
lineHeight: 1.35, minHeight: 118,
whiteSpace: "pre",
overflow: "hidden",
}} }}
> >
{detail?.diagram ?? "—"} {(() => {
</pre> const Ill = TOPO_ILLUSTRATION[entry.kind];
return Ill ? <Ill /> : (
<span style={{ fontFamily: mono, fontSize: 10.5, color: "#6a6a72" }}></span>
);
})()}
</div>
</div> </div>
); );
} }