dashboard: expand to 5 peer tiers — add Research + Loops with placeholder canvases
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 35s
ci / rust (push) Successful in 3m4s
ci / publish (push) Successful in 36s
ci / e2e (push) Failing after 29m22s

Fifth commit of the Research + Loops arc. Lights up the tier rail (both
the chip column and the top-bar crumbs) with two new peer surfaces:

  [VIZ] [RESEARCH] [LOOPS] [AGENT] [INFRA]

The stubs (ResearchList / ResearchCanvas / LoopsList / LoopsCanvas) are
self-contained placeholder components — each renders a header with a
"0 topics" / "0 loops" chip and a "coming soon" body plus a canvas hero
with the tier motif. Real functionality (topic cards, wizard, canvas
timelines, publish gate UI) arrives in the next two commits, and slots
in by replacing the stub files without touching Dashboard.tsx again.

Dashboard.tsx changes:
- `Tier` type widened to include `"research" | "loops"`.
- railIcon record grew two SVG entries — book+lens for Research, orbit
  arrows for Loops. Matches the placeholder canvas hero.
- TIER_TABS reordered as Viz → Research → Loops → Agent → Infra to
  match the mental grouping (conceptual work first, machinery second).
- Top-bar crumbs mirror the rail order.
- New isResearch / isLoops selectors used everywhere the existing tiers
  were tested; the Agents-header conditional now also skips for the new
  tiers so ResearchList and LoopsList can supply their own headers.
- Sidebar swap short-circuits to ResearchList / LoopsList; canvas swap
  short-circuits to ResearchCanvas / LoopsCanvas.
This commit is contained in:
Omar Sobh
2026-07-06 06:47:37 -07:00
parent 973eeb272e
commit 2ff00934b2
5 changed files with 333 additions and 6 deletions
@@ -23,6 +23,10 @@ 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, clawNode, type TreeItem } from "./StructureTree"; import { StructureTree, orgNode, clawNode, type TreeItem } from "./StructureTree";
import { ResearchList } from "./ResearchList";
import { ResearchCanvas } from "./ResearchCanvas";
import { LoopsList } from "./LoopsList";
import { LoopsCanvas } from "./LoopsCanvas";
import { UserMenu } from "./UserMenu"; import { UserMenu } from "./UserMenu";
import { ToolPanel, type ToolKey } from "./ToolPanel"; import { ToolPanel, type ToolKey } from "./ToolPanel";
import { InfraNav, FleetConsole, FleetStatusBar, FleetPill } from "./fleet/FleetConsole"; import { InfraNav, FleetConsole, FleetStatusBar, FleetPill } from "./fleet/FleetConsole";
@@ -43,7 +47,7 @@ import { DeviceSizeToggle } from "@/components/computer/DeviceSizeToggle";
const COMPUTER_WIDTH: Record<DeviceSize, string> = { phone: "448px", tablet: "67%", full: "100%" }; const COMPUTER_WIDTH: Record<DeviceSize, string> = { phone: "448px", tablet: "67%", full: "100%" };
type Tier = "world" | "claw" | "infra"; type Tier = "world" | "research" | "loops" | "claw" | "infra";
const mono = "'JetBrains Mono', ui-monospace, monospace"; const mono = "'JetBrains Mono', ui-monospace, monospace";
// Gradient palette for structure nodes that don't carry their own (companies, // Gradient palette for structure nodes that don't carry their own (companies,
@@ -79,11 +83,19 @@ const COMPANY_TEMPLATES: Template[] = [
const railIcon: Record<Tier, React.ReactNode> = { const railIcon: Record<Tier, React.ReactNode> = {
world: (<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="3.5" r="1.9" fill="currentColor" /><circle cx="3.8" cy="11" r="1.9" fill="currentColor" /><circle cx="16.2" cy="11" r="1.9" fill="currentColor" /><circle cx="10" cy="16.5" r="1.9" fill="currentColor" /><path d="M10 3.5 L3.8 11 M10 3.5 L16.2 11 M3.8 11 L10 16.5 M16.2 11 L10 16.5" stroke="currentColor" strokeWidth="1.1" opacity=".5" /></svg>), world: (<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="3.5" r="1.9" fill="currentColor" /><circle cx="3.8" cy="11" r="1.9" fill="currentColor" /><circle cx="16.2" cy="11" r="1.9" fill="currentColor" /><circle cx="10" cy="16.5" r="1.9" fill="currentColor" /><path d="M10 3.5 L3.8 11 M10 3.5 L16.2 11 M3.8 11 L10 16.5 M16.2 11 L10 16.5" stroke="currentColor" strokeWidth="1.1" opacity=".5" /></svg>),
// Book with a magnifying-lens tucked into the lower-right — research.
research: (<svg width="20" height="20" viewBox="0 0 24 24"><rect x="4" y="3.5" width="12" height="14" rx="1.5" stroke="currentColor" strokeWidth="1.4" fill="none" /><path d="M4 15.5 H16" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="17.5" cy="17.5" r="3.2" stroke="currentColor" strokeWidth="1.6" fill="none" /><path d="M19.9 19.9 L22 22" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>),
// Circular arrow ⟳ with an inner dot — loops.
loops: (<svg width="20" height="20" viewBox="0 0 24 24"><path d="M20 12 A8 8 0 1 1 12 4" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" /><path d="M13 3 L20 4 L18.5 10" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinejoin="round" strokeLinecap="round" /><circle cx="12" cy="12" r="1.8" fill="currentColor" /></svg>),
claw: (<svg width="20" height="20" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="10" cy="10" r="2.4" fill="currentColor" /></svg>), claw: (<svg width="20" height="20" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="10" cy="10" r="2.4" fill="currentColor" /></svg>),
infra: (<svg width="20" height="20" viewBox="0 0 20 20"><rect x="3.5" y="4" width="13" height="5" rx="1.4" stroke="currentColor" strokeWidth="1.4" fill="none" /><rect x="3.5" y="11" width="13" height="5" rx="1.4" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="6.4" cy="6.5" r="1" fill="currentColor" /><circle cx="6.4" cy="13.5" r="1" fill="currentColor" /></svg>), infra: (<svg width="20" height="20" viewBox="0 0 20 20"><rect x="3.5" y="4" width="13" height="5" rx="1.4" stroke="currentColor" strokeWidth="1.4" fill="none" /><rect x="3.5" y="11" width="13" height="5" rx="1.4" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="6.4" cy="6.5" r="1" fill="currentColor" /><circle cx="6.4" cy="13.5" r="1" fill="currentColor" /></svg>),
}; };
const TIER_TABS: { key: Tier; label: string }[] = [ const TIER_TABS: { key: Tier; label: string }[] = [
{ key: "world", label: "VIZ" }, { key: "claw", label: "AGENT" }, { key: "infra", label: "INFRA" }, { key: "world", label: "VIZ" },
{ key: "research", label: "RESEARCH" },
{ key: "loops", label: "LOOPS" },
{ key: "claw", label: "AGENT" },
{ key: "infra", label: "INFRA" },
]; ];
const companyIcon = (size: number) => ( const companyIcon = (size: number) => (
<svg width={size} height={size} viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg> <svg width={size} height={size} viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>
@@ -447,7 +459,11 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
const [reapOpen, setReapOpen] = useState(false); const [reapOpen, setReapOpen] = useState(false);
const [toolOpen, setToolOpen] = useState<ToolKey | null>(null); const [toolOpen, setToolOpen] = useState<ToolKey | null>(null);
const isWorld = tier === "world", isClaw = tier === "claw", isInfra = tier === "infra"; const isWorld = tier === "world",
isResearch = tier === "research",
isLoops = tier === "loops",
isClaw = tier === "claw",
isInfra = tier === "infra";
const allAgents = orgs.flatMap((o) => o.companies.flatMap((c) => c.teams.flatMap((t) => t.agents))); const allAgents = orgs.flatMap((o) => o.companies.flatMap((c) => c.teams.flatMap((t) => t.agents)));
// World: the full expandable org→company→team→agent forest. Agents page: a flat list. // World: the full expandable org→company→team→agent forest. Agents page: a flat list.
@@ -518,6 +534,10 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<div style={{ display: "flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 12 }}> <div style={{ display: "flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 12 }}>
<span style={crumbStyle(isWorld)} onClick={() => setTier("world")}>Visualizations</span> <span style={crumbStyle(isWorld)} onClick={() => setTier("world")}>Visualizations</span>
<span style={{ color: "#3a3a40" }}>/</span> <span style={{ color: "#3a3a40" }}>/</span>
<span style={crumbStyle(isResearch)} onClick={() => setTier("research")}>Research</span>
<span style={{ color: "#3a3a40" }}>/</span>
<span style={crumbStyle(isLoops)} onClick={() => setTier("loops")}>Loops</span>
<span style={{ color: "#3a3a40" }}>/</span>
<span style={crumbStyle(isClaw)} onClick={() => setTier("claw")}>Agents</span> <span style={crumbStyle(isClaw)} onClick={() => setTier("claw")}>Agents</span>
<span style={{ color: "#3a3a40" }}>/</span> <span style={{ color: "#3a3a40" }}>/</span>
<span style={crumbStyle(isInfra)} onClick={() => setTier("infra")}>Infrastructure</span> <span style={crumbStyle(isInfra)} onClick={() => setTier("infra")}>Infrastructure</span>
@@ -564,7 +584,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
</div> </div>
<button type="button" onClick={() => { setSelectMode((v) => { if (v) setSelectedAgents(new Set()); return !v; }); }} title="Select to manage" aria-label="Select to manage" style={{ flex: "none", width: 34, height: 34, borderRadius: 9, border: `1px solid ${selectMode ? "rgba(255,111,97,.5)" : "rgba(255,255,255,.12)"}`, background: selectMode ? "rgba(255,111,97,.12)" : "transparent", color: selectMode ? "#ff6f61" : "#9a9aa2", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}><Wrench aria-hidden size={16} /></button> <button type="button" onClick={() => { setSelectMode((v) => { if (v) setSelectedAgents(new Set()); return !v; }); }} title="Select to manage" aria-label="Select to manage" style={{ flex: "none", width: 34, height: 34, borderRadius: 9, border: `1px solid ${selectMode ? "rgba(255,111,97,.5)" : "rgba(255,255,255,.12)"}`, background: selectMode ? "rgba(255,111,97,.12)" : "transparent", color: selectMode ? "#ff6f61" : "#9a9aa2", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}><Wrench aria-hidden size={16} /></button>
</div> </div>
) : isInfra ? null : ( ) : isInfra || isResearch || isLoops ? null : (
<div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)", display: "flex", alignItems: "flex-start", gap: 8 }}> <div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)", display: "flex", alignItems: "flex-start", gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 6 }}>{allAgents.length} AGENT{allAgents.length === 1 ? "" : "S"}</div> <div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 6 }}>{allAgents.length} AGENT{allAgents.length === 1 ? "" : "S"}</div>
@@ -578,7 +598,11 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
</div> </div>
)} )}
{isInfra ? ( {isResearch ? (
<ResearchList />
) : isLoops ? (
<LoopsList />
) : isInfra ? (
<InfraNav view={infraSel ?? "local"} onSelect={setInfraSel} onConnectHost={() => setInfraConnectOpen(true)} /> <InfraNav view={infraSel ?? "local"} onSelect={setInfraSel} onConnectHost={() => setInfraConnectOpen(true)} />
) : ( ) : (
<> <>
@@ -605,7 +629,11 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
{/* CANVAS */} {/* CANVAS */}
<div ref={canvasRef} style={{ flex: 1, position: "relative", minWidth: 0, overflow: "hidden", ...(resizing ? { ["--duration-normal" as string]: "0ms" } : {}), ...(isClaw || isInfra ? { ["--computer-width" as string]: computerOpen ? (customWidth != null ? `${customWidth}px` : COMPUTER_WIDTH[device]) : "0px" } : isWorld ? { ["--world-width" as string]: worldPanelOpen ? COMPUTER_WIDTH[worldSize] : "0px" } : {}) }}> <div ref={canvasRef} style={{ flex: 1, position: "relative", minWidth: 0, overflow: "hidden", ...(resizing ? { ["--duration-normal" as string]: "0ms" } : {}), ...(isClaw || isInfra ? { ["--computer-width" as string]: computerOpen ? (customWidth != null ? `${customWidth}px` : COMPUTER_WIDTH[device]) : "0px" } : isWorld ? { ["--world-width" as string]: worldPanelOpen ? COMPUTER_WIDTH[worldSize] : "0px" } : {}) }}>
{isWorld ? ( {isResearch ? (
<ResearchCanvas />
) : isLoops ? (
<LoopsCanvas />
) : isWorld ? (
<> <>
{/* Graph stage — condensed by the right slide-out's width. */} {/* Graph stage — condensed by the right slide-out's width. */}
<div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}> <div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}>
@@ -0,0 +1,77 @@
"use client";
// Canvas for the Loops tier — stub. The real canvas (topology preview +
// trigger config + iteration timeline + webhook signing card) arrives with
// the loops-frontend commit.
const mono =
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
export function LoopsCanvas() {
return (
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 48,
}}
>
<div
style={{
maxWidth: 460,
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 20,
}}
>
{/* Orbit-arrows: the loops motif from the rail */}
<svg width="48" height="48" viewBox="0 0 24 24" aria-hidden>
<path
d="M20 12 A8 8 0 1 1 12 4"
stroke="#ff6f61"
strokeWidth="1.7"
fill="none"
strokeLinecap="round"
/>
<path
d="M13 3 L20 4 L18.5 10"
stroke="#ff6f61"
strokeWidth="1.7"
fill="none"
strokeLinejoin="round"
strokeLinecap="round"
/>
<circle cx="12" cy="12" r="2.2" fill="currentColor" opacity=".6" />
</svg>
<div
style={{
fontSize: 22,
fontWeight: 700,
color: "#f3f3f5",
letterSpacing: "-.01em",
}}
>
Loops
</div>
<div
style={{
fontFamily: mono,
fontSize: 12,
color: "#8a8a92",
lineHeight: 1.6,
}}
>
Pick a loop on the left to see its topology, trigger config, and
iteration timeline. Full canvas ships next commit.
</div>
</div>
</div>
);
}
@@ -0,0 +1,66 @@
"use client";
// Sidebar for the Loops tier — stub. The real list (loop cards + status +
// next-fire chip + `+` opens the loop wizard) arrives in the loops-frontend
// commit.
const mono =
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
export function LoopsList() {
return (
<>
<div
style={{
padding: "16px 16px 12px",
borderBottom: "1px solid rgba(255,255,255,.06)",
display: "flex",
alignItems: "flex-start",
gap: 8,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontFamily: mono,
fontSize: 10,
letterSpacing: ".12em",
color: "#5a5a62",
marginBottom: 6,
}}
>
0 LOOPS
</div>
<div
style={{
fontSize: 18,
fontWeight: 700,
color: "#f3f3f5",
letterSpacing: "-.01em",
}}
>
Loops
</div>
</div>
</div>
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "40px 16px",
fontFamily: mono,
fontSize: 11,
color: "#5a5a62",
textAlign: "center",
lineHeight: 1.6,
}}
>
Loop cards land in the next commit. Recurring topology executions —
cron, on-completion, or webhook-triggered.
</div>
</>
);
}
@@ -0,0 +1,90 @@
"use client";
// Canvas for the Research tier — stub. The real canvas (selected topic's
// agents grid, run timeline, publish gate) arrives with the wizard commit.
const mono =
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
export function ResearchCanvas() {
return (
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 48,
}}
>
<div
style={{
maxWidth: 460,
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 20,
}}
>
{/* Book + magnifying-lens: the research motif from the rail */}
<svg width="48" height="48" viewBox="0 0 24 24" aria-hidden>
<rect
x="4"
y="3.5"
width="12"
height="14"
rx="1.5"
stroke="#5a5a62"
strokeWidth="1.4"
fill="none"
/>
<path
d="M4 15.5 H16"
stroke="#5a5a62"
strokeWidth="1.4"
fill="none"
/>
<circle
cx="17.5"
cy="17.5"
r="3.2"
stroke="#ff6f61"
strokeWidth="1.6"
fill="none"
/>
<path
d="M19.9 19.9 L22 22"
stroke="#ff6f61"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
<div
style={{
fontSize: 22,
fontWeight: 700,
color: "#f3f3f5",
letterSpacing: "-.01em",
}}
>
Research
</div>
<div
style={{
fontFamily: mono,
fontSize: 12,
color: "#8a8a92",
lineHeight: 1.6,
}}
>
Pick a topic on the left to see the agents assembling around it, the
run timeline, and the publish gate. Full canvas ships next commit.
</div>
</div>
</div>
);
}
@@ -0,0 +1,66 @@
"use client";
// Sidebar for the Research tier — stub for the shell-expansion commit. The
// real list (topics + status pills + `+` opens the wizard) lands in the
// next commit that also brings the API client and canvas panels.
const mono =
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
export function ResearchList() {
return (
<>
<div
style={{
padding: "16px 16px 12px",
borderBottom: "1px solid rgba(255,255,255,.06)",
display: "flex",
alignItems: "flex-start",
gap: 8,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontFamily: mono,
fontSize: 10,
letterSpacing: ".12em",
color: "#5a5a62",
marginBottom: 6,
}}
>
0 TOPICS
</div>
<div
style={{
fontSize: 18,
fontWeight: 700,
color: "#f3f3f5",
letterSpacing: "-.01em",
}}
>
Research
</div>
</div>
</div>
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "40px 16px",
fontFamily: mono,
fontSize: 11,
color: "#5a5a62",
textAlign: "center",
lineHeight: 1.6,
}}
>
Research topic cards land in the next commit. Wizard: capture, refine
with LLM, assemble outputs.
</div>
</>
);
}