mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-22 05:13:48 +08:00
Consolidate agent harness course into 19 lessons
This commit is contained in:
@@ -25,8 +25,6 @@ import s16Annotations from "@/data/annotations/s16.json";
|
||||
import s17Annotations from "@/data/annotations/s17.json";
|
||||
import s18Annotations from "@/data/annotations/s18.json";
|
||||
import s19Annotations from "@/data/annotations/s19.json";
|
||||
import s20Annotations from "@/data/annotations/s20.json";
|
||||
import s21Annotations from "@/data/annotations/s21.json";
|
||||
|
||||
interface Decision {
|
||||
id: string;
|
||||
@@ -62,8 +60,6 @@ const ANNOTATIONS: Record<string, AnnotationFile> = {
|
||||
s17: s17Annotations as AnnotationFile,
|
||||
s18: s18Annotations as AnnotationFile,
|
||||
s19: s19Annotations as AnnotationFile,
|
||||
s20: s20Annotations as AnnotationFile,
|
||||
s21: s21Annotations as AnnotationFile,
|
||||
};
|
||||
|
||||
interface DesignDecisionsProps {
|
||||
|
||||
@@ -28,8 +28,6 @@ const scenarioModules: Record<string, () => Promise<{ default: Scenario }>> = {
|
||||
s17: () => import("@/data/scenarios/s17.json") as Promise<{ default: Scenario }>,
|
||||
s18: () => import("@/data/scenarios/s18.json") as Promise<{ default: Scenario }>,
|
||||
s19: () => import("@/data/scenarios/s19.json") as Promise<{ default: Scenario }>,
|
||||
s20: () => import("@/data/scenarios/s20.json") as Promise<{ default: Scenario }>,
|
||||
s21: () => import("@/data/scenarios/s21.json") as Promise<{ default: Scenario }>,
|
||||
};
|
||||
|
||||
interface AgentLoopSimulatorProps {
|
||||
|
||||
@@ -21,11 +21,9 @@ const visualizations: Record<
|
||||
s12: lazy(() => import("./s07-task-system")),
|
||||
s13: lazy(() => import("./s08-background-tasks")),
|
||||
s14: lazy(() => import("./s14-cron-scheduler")),
|
||||
s15: lazy(() => import("./s10-team-protocols")),
|
||||
s16: lazy(() => import("./s11-autonomous-agents")),
|
||||
s17: lazy(() => import("./s12-worktree-task-isolation")),
|
||||
s18: lazy(() => import("./s19-mcp-tools")),
|
||||
s19: lazy(() => import("./s20-comprehensive")),
|
||||
s15: lazy(() => import("./s15-team-runtime")),
|
||||
s16: lazy(() => import("./s16-mcp-tools")),
|
||||
s17: lazy(() => import("./s17-integrated-harness")),
|
||||
};
|
||||
|
||||
export function SessionVisualization({ version }: { version: string }) {
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Inbox, MessageSquareText, UsersRound } from "lucide-react";
|
||||
import { StepControls } from "@/components/visualizations/shared/step-controls";
|
||||
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AgentId = "lead" | "coder" | "reviewer";
|
||||
|
||||
interface Mail {
|
||||
id: string;
|
||||
from: AgentId;
|
||||
to: AgentId;
|
||||
subject: string;
|
||||
body: string;
|
||||
appearsAt: number;
|
||||
consumedAt?: number;
|
||||
}
|
||||
|
||||
const AGENTS: { id: AgentId; label: string; role: string }[] = [
|
||||
{ id: "lead", label: "Lead", role: "splits work and reads results" },
|
||||
{ id: "coder", label: "Coder", role: "implements one slice" },
|
||||
{ id: "reviewer", label: "Reviewer", role: "checks the result" },
|
||||
];
|
||||
|
||||
const MAIL: Mail[] = [
|
||||
{
|
||||
id: "assign",
|
||||
from: "lead",
|
||||
to: "coder",
|
||||
subject: "Build login UI",
|
||||
body: "Please implement the login form and report back.",
|
||||
appearsAt: 1,
|
||||
consumedAt: 2,
|
||||
},
|
||||
{
|
||||
id: "result",
|
||||
from: "coder",
|
||||
to: "reviewer",
|
||||
subject: "Login UI done",
|
||||
body: "Files changed, ready for review.",
|
||||
appearsAt: 4,
|
||||
consumedAt: 5,
|
||||
},
|
||||
{
|
||||
id: "feedback",
|
||||
from: "reviewer",
|
||||
to: "lead",
|
||||
subject: "Review passed",
|
||||
body: "No blockers. One small polish note.",
|
||||
appearsAt: 5,
|
||||
},
|
||||
];
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
title: "A Team Is Mailboxes",
|
||||
desc: "Each agent has its own inbox file. The team does not need shared memory to coordinate.",
|
||||
},
|
||||
{
|
||||
title: "Lead Drops a Card",
|
||||
desc: "Assigning work means appending a message to the coder's inbox.",
|
||||
},
|
||||
{
|
||||
title: "Coder Reads Before Thinking",
|
||||
desc: "Before its next model call, the coder drains its inbox and turns messages into context.",
|
||||
},
|
||||
{
|
||||
title: "Coder Works Alone",
|
||||
desc: "The coder now runs its own loop. The lead does not have to hold the full context.",
|
||||
},
|
||||
{
|
||||
title: "Result Becomes Mail",
|
||||
desc: "The coder sends a result card to the reviewer through the same mailbox mechanism.",
|
||||
},
|
||||
{
|
||||
title: "Reviewer Sends Feedback",
|
||||
desc: "Review feedback is just another card. The lead reads it from its inbox.",
|
||||
},
|
||||
{
|
||||
title: "Files Are the Coordination Layer",
|
||||
desc: "The whole team is inspectable as append-only inbox files: lead.jsonl, coder.jsonl, reviewer.jsonl.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function visibleMail(agent: AgentId, step: number) {
|
||||
return MAIL.filter((mail) => mail.to === agent && mail.appearsAt <= step && (mail.consumedAt === undefined || step < mail.consumedAt));
|
||||
}
|
||||
|
||||
function agentState(agent: AgentId, step: number): "waiting" | "reading" | "working" | "reviewing" | "done" {
|
||||
if (agent === "lead" && step === 1) return "working";
|
||||
if (agent === "coder" && step === 2) return "reading";
|
||||
if (agent === "coder" && (step === 3 || step === 4)) return "working";
|
||||
if (agent === "reviewer" && step === 5) return "reviewing";
|
||||
if (agent === "lead" && step >= 5) return "reading";
|
||||
if (step === 6) return "done";
|
||||
return "waiting";
|
||||
}
|
||||
|
||||
function stateClass(state: ReturnType<typeof agentState>) {
|
||||
if (state === "working") return "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30";
|
||||
if (state === "reading" || state === "reviewing") return "border-amber-300 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30";
|
||||
if (state === "done") return "border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/30";
|
||||
return "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900";
|
||||
}
|
||||
|
||||
function MailCard({ mail }: { mail: Mail }) {
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, y: 8, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -6, scale: 0.98 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
className="rounded-md border border-amber-200 bg-amber-50 p-3 text-amber-900 shadow-sm dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-100"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="font-mono text-[11px] font-semibold">{mail.from} -> {mail.to}</span>
|
||||
<MessageSquareText size={14} />
|
||||
</div>
|
||||
<div className="text-sm font-semibold leading-snug">{mail.subject}</div>
|
||||
<div className="mt-1 text-xs leading-relaxed opacity-85">{mail.body}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentPanel({ agent, step }: { agent: (typeof AGENTS)[number]; step: number }) {
|
||||
const state = agentState(agent.id, step);
|
||||
const inbox = visibleMail(agent.id, step);
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-lg border p-3 transition-colors", stateClass(state))}>
|
||||
<div className="mb-3 flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="text-base font-bold text-zinc-900 dark:text-zinc-100">{agent.label}</div>
|
||||
<div className="text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">{agent.role}</div>
|
||||
</div>
|
||||
<span className="rounded-md bg-white px-2 py-1 text-[11px] font-semibold capitalize text-zinc-600 shadow-sm dark:bg-zinc-900 dark:text-zinc-300">
|
||||
{state}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-zinc-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<Inbox size={15} />
|
||||
{agent.id}.jsonl
|
||||
</div>
|
||||
<div className="min-h-[118px] space-y-2">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{inbox.length > 0 ? (
|
||||
inbox.map((mail) => <MailCard key={mail.id} mail={mail} />)
|
||||
) : (
|
||||
<motion.div
|
||||
key="empty"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="rounded-md border border-dashed border-zinc-300 px-3 py-8 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:text-zinc-400"
|
||||
>
|
||||
inbox empty
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActivityLog({ step }: { step: number }) {
|
||||
const items = [
|
||||
"team config creates lead, coder, reviewer",
|
||||
"lead appends task card to coder.jsonl",
|
||||
"coder drains inbox before model call",
|
||||
"coder works in its own loop",
|
||||
"coder appends result to reviewer.jsonl",
|
||||
"reviewer appends feedback to lead.jsonl",
|
||||
"all coordination remains visible on disk",
|
||||
].slice(0, step + 1);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800/70">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<UsersRound size={16} />
|
||||
What changed
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<motion.div
|
||||
key={item}
|
||||
initial={{ opacity: 0, x: 8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="rounded-md border border-zinc-200 bg-white px-3 py-2 text-xs text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200"
|
||||
>
|
||||
{item}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AgentTeams({ title }: { title?: string }) {
|
||||
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
|
||||
const step = vis.currentStep;
|
||||
const current = STEPS[step];
|
||||
|
||||
return (
|
||||
<section className="min-h-[500px] space-y-4">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title || "Agent Team Mailboxes"}
|
||||
</h2>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="grid gap-3 xl:grid-cols-[1fr_1fr_1fr_0.9fr]">
|
||||
{AGENTS.map((agent) => (
|
||||
<AgentPanel key={agent.id} agent={agent} step={step} />
|
||||
))}
|
||||
<ActivityLog step={step} />
|
||||
</div>
|
||||
|
||||
<StepControls
|
||||
className="mt-4"
|
||||
currentStep={vis.currentStep}
|
||||
totalSteps={vis.totalSteps}
|
||||
onPrev={vis.prev}
|
||||
onNext={vis.next}
|
||||
onReset={vis.reset}
|
||||
isPlaying={vis.isPlaying}
|
||||
onToggleAutoPlay={vis.toggleAutoPlay}
|
||||
stepTitle={current.title}
|
||||
stepDescription={current.desc}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ const MEMORY_FILES: MemoryFile[] = [
|
||||
title: "Verification commands",
|
||||
filename: "lcc_test_commands.md",
|
||||
description: "Useful smoke checks for the course website.",
|
||||
body: "Run npm run build, then browser-check /zh/s09 and /zh/s20.",
|
||||
body: "Run npm run build, then browser-check /zh/s09 and /zh/s19.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowRight, CheckCircle2, ClipboardCheck, FileText, LockKeyhole, UserCheck } from "lucide-react";
|
||||
import { StepControls } from "@/components/visualizations/shared/step-controls";
|
||||
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Protocol = "shutdown" | "plan";
|
||||
|
||||
const REQUEST_ID = "req_abc";
|
||||
|
||||
const SHUTDOWN_STEPS = [
|
||||
{
|
||||
title: "Agree on a Small Form",
|
||||
desc: "A protocol is just a shared card shape: request type, request_id, and the expected answer.",
|
||||
},
|
||||
{
|
||||
title: "Leader Files a Request",
|
||||
desc: "The leader writes a shutdown request card instead of force-stopping the teammate.",
|
||||
},
|
||||
{
|
||||
title: "Teammate Chooses",
|
||||
desc: "The teammate can approve or reject, and the request_id keeps the answer attached to the right request.",
|
||||
},
|
||||
{
|
||||
title: "Clean Exit",
|
||||
desc: "The approved response returns to the leader, and the teammate exits cleanly.",
|
||||
},
|
||||
];
|
||||
|
||||
const PLAN_STEPS = [
|
||||
{
|
||||
title: "Work Is Locked",
|
||||
desc: "In plan mode, implementation stays locked until a plan card is approved.",
|
||||
},
|
||||
{
|
||||
title: "Submit the Plan Card",
|
||||
desc: "The teammate sends a concrete plan with the same request-response shape.",
|
||||
},
|
||||
{
|
||||
title: "Approval Unlocks Action",
|
||||
desc: "The leader approves the card, then implementation can begin.",
|
||||
},
|
||||
];
|
||||
|
||||
const PROTOCOL_STATES: Record<Protocol, { label: string; detail: string }[]> = {
|
||||
shutdown: [
|
||||
{ label: "drafted", detail: "Lead creates request_id" },
|
||||
{ label: "pending", detail: "card waits in inbox" },
|
||||
{ label: "deciding", detail: "teammate replies" },
|
||||
{ label: "closed", detail: "Lead matches response" },
|
||||
],
|
||||
plan: [
|
||||
{ label: "locked", detail: "work cannot start" },
|
||||
{ label: "submitted", detail: "plan card is sent" },
|
||||
{ label: "approved", detail: "implementation unlocks" },
|
||||
],
|
||||
};
|
||||
|
||||
function ToggleButton({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"rounded-md px-3 py-1.5 text-xs font-medium transition-colors active:scale-95",
|
||||
active
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function StateRail({
|
||||
states,
|
||||
currentStep,
|
||||
}: {
|
||||
states: { label: string; detail: string }[];
|
||||
currentStep: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800/70">
|
||||
<div className="mb-3 flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-sm font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
Protocol state
|
||||
</div>
|
||||
<div className="break-words font-mono text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
request_id: {REQUEST_ID}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-stretch">
|
||||
{states.map((state, index) => {
|
||||
const active = index === currentStep;
|
||||
const done = index < currentStep;
|
||||
return (
|
||||
<div key={state.label} className="flex min-w-0 flex-1 items-stretch gap-2">
|
||||
<motion.div
|
||||
layout
|
||||
animate={active ? { y: [0, -2, 0] } : { y: 0 }}
|
||||
transition={{ duration: 0.8, repeat: active ? Infinity : 0 }}
|
||||
className={cn(
|
||||
"min-w-0 flex-1 rounded-md border px-3 py-2 transition-colors",
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-950/35 dark:text-blue-200"
|
||||
: done
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/30 dark:text-emerald-200"
|
||||
: "border-zinc-200 bg-white text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400"
|
||||
)}
|
||||
>
|
||||
<div className="break-words text-sm font-semibold">{state.label}</div>
|
||||
<div className="mt-1 break-words text-[11px] leading-snug opacity-80">
|
||||
{state.detail}
|
||||
</div>
|
||||
</motion.div>
|
||||
{index < states.length - 1 && (
|
||||
<div className="hidden items-center text-zinc-300 dark:text-zinc-600 sm:flex">
|
||||
<ArrowRight size={15} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Desk({
|
||||
title,
|
||||
icon,
|
||||
active,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
active: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-[260px] rounded-lg border p-3 transition-colors",
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30"
|
||||
: "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex min-w-0 items-center gap-2 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-7 w-7 items-center justify-center rounded-md",
|
||||
active
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span className="min-w-0 break-words">{title}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProtocolCard({
|
||||
title,
|
||||
rows,
|
||||
tone = "blue",
|
||||
}: {
|
||||
title: string;
|
||||
rows: string[];
|
||||
tone?: "blue" | "amber" | "emerald" | "zinc";
|
||||
}) {
|
||||
const toneClass = {
|
||||
blue: "border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-900 dark:bg-blue-950/40 dark:text-blue-200",
|
||||
amber: "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-200",
|
||||
emerald:
|
||||
"border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-200",
|
||||
zinc: "border-zinc-200 bg-zinc-50 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200",
|
||||
}[tone];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, y: 10, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -8, scale: 0.98 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className={cn("rounded-md border p-3 shadow-sm", toneClass)}
|
||||
>
|
||||
<div className="break-words font-mono text-xs font-semibold">{title}</div>
|
||||
<div className="mt-2 space-y-1 font-mono text-[11px] opacity-85">
|
||||
{rows.map((row) => (
|
||||
<div key={row} className="break-words">
|
||||
{row}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyTray({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-zinc-300 px-3 py-5 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamProtocols({ title }: { title?: string }) {
|
||||
const [protocol, setProtocol] = useState<Protocol>("shutdown");
|
||||
const steps = protocol === "shutdown" ? SHUTDOWN_STEPS : PLAN_STEPS;
|
||||
const vis = useSteppedVisualization({ totalSteps: steps.length, autoPlayInterval: 2500 });
|
||||
const step = vis.currentStep;
|
||||
|
||||
const switchProtocol = (value: Protocol) => {
|
||||
setProtocol(value);
|
||||
vis.reset();
|
||||
};
|
||||
|
||||
const isPlan = protocol === "plan";
|
||||
|
||||
return (
|
||||
<section className="min-h-[500px] space-y-4">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title || "Team Protocol Cards"}
|
||||
</h2>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="mb-4 flex justify-center gap-2">
|
||||
<ToggleButton active={!isPlan} onClick={() => switchProtocol("shutdown")}>
|
||||
Shutdown
|
||||
</ToggleButton>
|
||||
<ToggleButton active={isPlan} onClick={() => switchProtocol("plan")}>
|
||||
Plan Approval
|
||||
</ToggleButton>
|
||||
</div>
|
||||
|
||||
<StateRail states={PROTOCOL_STATES[protocol]} currentStep={step} />
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
<Desk
|
||||
title="Leader desk"
|
||||
icon={<UserCheck size={15} />}
|
||||
active={(!isPlan && (step === 1 || step === 3)) || (isPlan && step === 2)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{!isPlan && step >= 1 && (
|
||||
<ProtocolCard
|
||||
key="shutdown-request"
|
||||
title="shutdown_request"
|
||||
rows={[`request_id: ${REQUEST_ID}`, "target: teammate", "mode: polite"]}
|
||||
tone={step >= 3 ? "zinc" : "blue"}
|
||||
/>
|
||||
)}
|
||||
{!isPlan && step >= 3 && (
|
||||
<ProtocolCard
|
||||
key="shutdown-response"
|
||||
title="shutdown_response"
|
||||
rows={[`request_id: ${REQUEST_ID}`, "approve: true", "status: closed"]}
|
||||
tone="emerald"
|
||||
/>
|
||||
)}
|
||||
{isPlan && step >= 2 && (
|
||||
<ProtocolCard
|
||||
key="plan-approved"
|
||||
title="plan_approval_response"
|
||||
rows={[`request_id: ${REQUEST_ID}`, "approve: true", "unlock: implementation"]}
|
||||
tone="emerald"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{((!isPlan && step === 0) || (isPlan && step < 2)) && (
|
||||
<EmptyTray label="waiting for a protocol card" />
|
||||
)}
|
||||
</div>
|
||||
</Desk>
|
||||
|
||||
<Desk
|
||||
title="Shared card shape"
|
||||
icon={<ClipboardCheck size={15} />}
|
||||
active={(!isPlan && step === 0) || (isPlan && step === 0)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ProtocolCard
|
||||
title="protocol fields"
|
||||
rows={["type", "request_id", "payload", "response"]}
|
||||
tone="amber"
|
||||
/>
|
||||
<div className="rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
The key idea is correlation, not ceremony.
|
||||
</div>
|
||||
{isPlan && (
|
||||
<div className="flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-xs text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300">
|
||||
<LockKeyhole size={14} />
|
||||
implementation locked until approval
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Desk>
|
||||
|
||||
<Desk
|
||||
title="Teammate desk"
|
||||
icon={isPlan ? <FileText size={15} /> : <CheckCircle2 size={15} />}
|
||||
active={(!isPlan && step === 2) || (isPlan && step === 1)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{!isPlan && step >= 2 && (
|
||||
<ProtocolCard
|
||||
key="teammate-decision"
|
||||
title="decision card"
|
||||
rows={[`request_id: ${REQUEST_ID}`, "choice: approve", step >= 3 ? "state: exited" : "state: deciding"]}
|
||||
tone={step >= 3 ? "emerald" : "amber"}
|
||||
/>
|
||||
)}
|
||||
{isPlan && step >= 1 && (
|
||||
<ProtocolCard
|
||||
key="plan-card"
|
||||
title="exit_plan_mode"
|
||||
rows={[`request_id: ${REQUEST_ID}`, "1. edit module", "2. run tests", "3. report diff"]}
|
||||
tone={step >= 2 ? "emerald" : "blue"}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{((!isPlan && step < 2) || (isPlan && step === 0)) && (
|
||||
<EmptyTray label={isPlan ? "draft plan not submitted" : "no request received"} />
|
||||
)}
|
||||
</div>
|
||||
</Desk>
|
||||
</div>
|
||||
|
||||
<StepControls
|
||||
className="mt-4"
|
||||
currentStep={vis.currentStep}
|
||||
totalSteps={vis.totalSteps}
|
||||
onPrev={vis.prev}
|
||||
onNext={vis.next}
|
||||
onReset={vis.reset}
|
||||
isPlaying={vis.isPlaying}
|
||||
onToggleAutoPlay={vis.toggleAutoPlay}
|
||||
stepTitle={steps[step].title}
|
||||
stepDescription={steps[step].desc}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { CheckCircle2, ClipboardList, Hourglass, UserRoundCog } from "lucide-react";
|
||||
import { StepControls } from "@/components/visualizations/shared/step-controls";
|
||||
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AgentPhase = "idle" | "polling" | "claiming" | "working" | "done";
|
||||
type TaskStatus = "open" | "claimed" | "complete";
|
||||
|
||||
interface AgentState {
|
||||
id: string;
|
||||
phase: AgentPhase;
|
||||
timer: number;
|
||||
task?: string;
|
||||
}
|
||||
|
||||
interface TaskState {
|
||||
id: string;
|
||||
title: string;
|
||||
status: TaskStatus;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
title: "Quiet Agents",
|
||||
desc: "Autonomous agents start by waiting. The important mental model is a work board, not a central dispatcher.",
|
||||
},
|
||||
{
|
||||
title: "Idle Timer Fills",
|
||||
desc: "An agent watches its own idle timer. When it waits long enough, it decides to look for work.",
|
||||
},
|
||||
{
|
||||
title: "Read the Board",
|
||||
desc: "The agent polls the shared task board and looks for an open card.",
|
||||
},
|
||||
{
|
||||
title: "Claim One Card",
|
||||
desc: "Claiming writes the agent name onto one task, making ownership visible.",
|
||||
},
|
||||
{
|
||||
title: "Work Independently",
|
||||
desc: "The claimed task moves into the agent workspace. No coordinator has to babysit it.",
|
||||
},
|
||||
{
|
||||
title: "Others Join In",
|
||||
desc: "A second agent can claim a different card through the same simple habit.",
|
||||
},
|
||||
{
|
||||
title: "Finish and Free Up",
|
||||
desc: "Completed work goes back to the board as done, and the agent returns to waiting.",
|
||||
},
|
||||
{
|
||||
title: "Self Organization",
|
||||
desc: "Timers plus visible ownership let a small group organize itself without a manager loop.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const TASKS = [
|
||||
{ id: "T1", title: "Fix auth bug" },
|
||||
{ id: "T2", title: "Add rate limiter" },
|
||||
{ id: "T3", title: "Write docs" },
|
||||
{ id: "T4", title: "Clean tests" },
|
||||
];
|
||||
|
||||
function getAgents(step: number): AgentState[] {
|
||||
if (step === 0) {
|
||||
return [
|
||||
{ id: "A", phase: "idle", timer: 0.1 },
|
||||
{ id: "B", phase: "idle", timer: 0 },
|
||||
{ id: "C", phase: "idle", timer: 0 },
|
||||
];
|
||||
}
|
||||
if (step === 1) {
|
||||
return [
|
||||
{ id: "A", phase: "idle", timer: 0.85 },
|
||||
{ id: "B", phase: "idle", timer: 0.25 },
|
||||
{ id: "C", phase: "idle", timer: 0 },
|
||||
];
|
||||
}
|
||||
if (step === 2) {
|
||||
return [
|
||||
{ id: "A", phase: "polling", timer: 1 },
|
||||
{ id: "B", phase: "idle", timer: 0.25 },
|
||||
{ id: "C", phase: "idle", timer: 0 },
|
||||
];
|
||||
}
|
||||
if (step === 3) {
|
||||
return [
|
||||
{ id: "A", phase: "claiming", timer: 0, task: "T1" },
|
||||
{ id: "B", phase: "idle", timer: 0.45 },
|
||||
{ id: "C", phase: "idle", timer: 0.1 },
|
||||
];
|
||||
}
|
||||
if (step === 4) {
|
||||
return [
|
||||
{ id: "A", phase: "working", timer: 0, task: "T1" },
|
||||
{ id: "B", phase: "idle", timer: 0.65 },
|
||||
{ id: "C", phase: "idle", timer: 0.2 },
|
||||
];
|
||||
}
|
||||
if (step === 5) {
|
||||
return [
|
||||
{ id: "A", phase: "working", timer: 0, task: "T1" },
|
||||
{ id: "B", phase: "claiming", timer: 0, task: "T2" },
|
||||
{ id: "C", phase: "idle", timer: 0.35 },
|
||||
];
|
||||
}
|
||||
if (step === 6) {
|
||||
return [
|
||||
{ id: "A", phase: "done", timer: 0, task: "T1" },
|
||||
{ id: "B", phase: "working", timer: 0, task: "T2" },
|
||||
{ id: "C", phase: "idle", timer: 0.6 },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ id: "A", phase: "idle", timer: 0.15 },
|
||||
{ id: "B", phase: "working", timer: 0, task: "T2" },
|
||||
{ id: "C", phase: "claiming", timer: 0, task: "T3" },
|
||||
];
|
||||
}
|
||||
|
||||
function getTasks(step: number): TaskState[] {
|
||||
return TASKS.map((task) => {
|
||||
if (task.id === "T1" && step >= 6) {
|
||||
return { ...task, status: "complete", owner: "A" };
|
||||
}
|
||||
if (task.id === "T1" && step >= 3) {
|
||||
return { ...task, status: "claimed", owner: "A" };
|
||||
}
|
||||
if (task.id === "T2" && step >= 5) {
|
||||
return { ...task, status: "claimed", owner: "B" };
|
||||
}
|
||||
if (task.id === "T3" && step >= 7) {
|
||||
return { ...task, status: "claimed", owner: "C" };
|
||||
}
|
||||
return { ...task, status: "open" };
|
||||
});
|
||||
}
|
||||
|
||||
function phaseClass(phase: AgentPhase): string {
|
||||
if (phase === "working") return "border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/30";
|
||||
if (phase === "claiming" || phase === "polling") return "border-amber-300 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30";
|
||||
if (phase === "done") return "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30";
|
||||
return "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900";
|
||||
}
|
||||
|
||||
function statusClass(status: TaskStatus): string {
|
||||
if (status === "complete") return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300";
|
||||
if (status === "claimed") return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
|
||||
return "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300";
|
||||
}
|
||||
|
||||
function AgentCard({ agent }: { agent: AgentState }) {
|
||||
const timerPercent = Math.round(agent.timer * 100);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
animate={agent.phase !== "idle" ? { y: [0, -2, 0] } : { y: 0 }}
|
||||
transition={{ duration: 0.9, repeat: agent.phase !== "idle" ? Infinity : 0 }}
|
||||
className={cn("rounded-lg border p-3 transition-colors", phaseClass(agent.phase))}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 text-sm font-bold text-white dark:bg-zinc-100 dark:text-zinc-900">
|
||||
{agent.id}
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-zinc-800 dark:text-zinc-100">Agent {agent.id}</div>
|
||||
<div className="text-[11px] capitalize text-zinc-500 dark:text-zinc-400">{agent.phase}</div>
|
||||
</div>
|
||||
</div>
|
||||
{agent.phase === "done" ? (
|
||||
<CheckCircle2 size={18} className="text-emerald-500" />
|
||||
) : (
|
||||
<UserRoundCog size={18} className="text-zinc-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-2 h-2 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-amber-400"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${timerPercent}%` }}
|
||||
transition={{ duration: 0.35 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="font-mono text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
{agent.task ? `task: ${agent.task}` : `idle timer: ${timerPercent}%`}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AutonomousAgents({ title }: { title?: string }) {
|
||||
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
|
||||
const step = vis.currentStep;
|
||||
const agents = getAgents(step);
|
||||
const tasks = getTasks(step);
|
||||
const current = STEPS[step];
|
||||
|
||||
return (
|
||||
<section className="min-h-[500px] space-y-4">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title || "Autonomous Work Board"}
|
||||
</h2>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="grid gap-3 lg:grid-cols-[1fr_1.2fr]">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<Hourglass size={16} />
|
||||
Agents watch their own idle timer
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-3 lg:grid-cols-1">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard key={`${agent.id}-${agent.phase}-${agent.task ?? "none"}-${step}`} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800/70">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<ClipboardList size={16} />
|
||||
Shared task board
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{tasks.map((task) => (
|
||||
<motion.div
|
||||
layout
|
||||
key={`${task.id}-${task.status}-${task.owner ?? "open"}`}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.96 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="rounded-md border border-zinc-200 bg-white p-3 text-xs shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<span className="font-mono font-semibold text-zinc-500 dark:text-zinc-400">{task.id}</span>
|
||||
<span className={cn("rounded px-1.5 py-0.5 text-[10px] font-semibold", statusClass(task.status))}>
|
||||
{task.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-medium text-zinc-800 dark:text-zinc-100">{task.title}</div>
|
||||
<div className="mt-2 font-mono text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
owner: {task.owner ?? "-"}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="mt-3 rounded-md border border-zinc-200 bg-white px-3 py-2 text-xs text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300">
|
||||
Nobody assigns tasks directly; agents claim visible open cards when their timers wake them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StepControls
|
||||
className="mt-4"
|
||||
currentStep={vis.currentStep}
|
||||
totalSteps={vis.totalSteps}
|
||||
onPrev={vis.prev}
|
||||
onNext={vis.next}
|
||||
onReset={vis.reset}
|
||||
isPlaying={vis.isPlaying}
|
||||
onToggleAutoPlay={vis.toggleAutoPlay}
|
||||
stepTitle={current.title}
|
||||
stepDescription={current.desc}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
|
||||
import { StepControls } from "@/components/visualizations/shared/step-controls";
|
||||
|
||||
type TaskStatus = "pending" | "in_progress" | "completed";
|
||||
|
||||
interface TaskRow {
|
||||
id: number;
|
||||
subject: string;
|
||||
status: TaskStatus;
|
||||
worktree: string;
|
||||
}
|
||||
|
||||
interface WorktreeRow {
|
||||
name: string;
|
||||
branch: string;
|
||||
task: string;
|
||||
state: "none" | "active" | "kept" | "removed";
|
||||
}
|
||||
|
||||
interface Lane {
|
||||
name: string;
|
||||
files: string[];
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
interface StepState {
|
||||
title: string;
|
||||
desc: string;
|
||||
tasks: TaskRow[];
|
||||
worktrees: WorktreeRow[];
|
||||
lanes: Lane[];
|
||||
op: string;
|
||||
}
|
||||
|
||||
const STEPS: StepState[] = [
|
||||
{
|
||||
title: "Single Workspace Pain",
|
||||
desc: "Two tasks are active, but both edits would hit one directory and collide.",
|
||||
op: "task_create x2",
|
||||
tasks: [
|
||||
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "" },
|
||||
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "" },
|
||||
],
|
||||
worktrees: [],
|
||||
lanes: [
|
||||
{ name: "main", files: ["auth/service.py", "ui/Login.tsx"], highlight: true },
|
||||
{ name: "wt/auth-refactor", files: [] },
|
||||
{ name: "wt/ui-login", files: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Allocate Lane for Task 1",
|
||||
desc: "Create a worktree lane and associate it with task 1 for clear ownership.",
|
||||
op: "worktree_create(name='auth-refactor', task_id=1)",
|
||||
tasks: [
|
||||
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
|
||||
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "" },
|
||||
],
|
||||
worktrees: [
|
||||
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
|
||||
],
|
||||
lanes: [
|
||||
{ name: "main", files: ["ui/Login.tsx"] },
|
||||
{ name: "wt/auth-refactor", files: ["auth/service.py"], highlight: true },
|
||||
{ name: "wt/ui-login", files: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Allocate Lane for Task 2",
|
||||
desc: "Lane creation and task association can be separate. Here task 2 binds after lane creation.",
|
||||
op: "worktree_create(name='ui-login')\ntask_bind_worktree(task_id=2, worktree='ui-login')",
|
||||
tasks: [
|
||||
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
|
||||
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
|
||||
],
|
||||
worktrees: [
|
||||
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
|
||||
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "active" },
|
||||
],
|
||||
lanes: [
|
||||
{ name: "main", files: [] },
|
||||
{ name: "wt/auth-refactor", files: ["auth/service.py"] },
|
||||
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Run Commands in Isolated Lanes",
|
||||
desc: "Each command routes by selected lane directory, not by the shared root.",
|
||||
op: "worktree_run('auth-refactor', 'pytest tests/auth -q')",
|
||||
tasks: [
|
||||
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
|
||||
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
|
||||
],
|
||||
worktrees: [
|
||||
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
|
||||
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "active" },
|
||||
],
|
||||
lanes: [
|
||||
{ name: "main", files: [] },
|
||||
{ name: "wt/auth-refactor", files: ["auth/service.py", "tests/auth/test_login.py"], highlight: true },
|
||||
{ name: "wt/ui-login", files: ["ui/Login.tsx", "ui/Login.css"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Keep One Lane, Close Another",
|
||||
desc: "Closeout can mix decisions: keep ui-login active for follow-up, remove auth-refactor and complete task 1.",
|
||||
op: "worktree_keep('ui-login')\nworktree_remove('auth-refactor', complete_task=true)\nworktree_events(limit=10)",
|
||||
tasks: [
|
||||
{ id: 1, subject: "Auth refactor", status: "completed", worktree: "" },
|
||||
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
|
||||
],
|
||||
worktrees: [
|
||||
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "removed" },
|
||||
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "kept" },
|
||||
],
|
||||
lanes: [
|
||||
{ name: "main", files: [] },
|
||||
{ name: "wt/auth-refactor", files: [] },
|
||||
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Isolation + Coordination + Events",
|
||||
desc: "The board tracks shared truth, worktree lanes isolate execution, and events provide auditable side-channel traces.",
|
||||
op: "task_list + worktree_list + worktree_events",
|
||||
tasks: [
|
||||
{ id: 1, subject: "Auth refactor", status: "completed", worktree: "" },
|
||||
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
|
||||
],
|
||||
worktrees: [
|
||||
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "removed" },
|
||||
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "kept" },
|
||||
],
|
||||
lanes: [
|
||||
{ name: "main", files: [] },
|
||||
{ name: "wt/auth-refactor", files: [] },
|
||||
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function statusClass(status: TaskStatus): string {
|
||||
if (status === "completed") return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300";
|
||||
if (status === "in_progress") return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
|
||||
return "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300";
|
||||
}
|
||||
|
||||
function worktreeClass(state: WorktreeRow["state"]): string {
|
||||
if (state === "active") return "border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900/20";
|
||||
if (state === "kept") return "border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20";
|
||||
if (state === "removed") return "border-zinc-200 bg-zinc-100 opacity-70 dark:border-zinc-700 dark:bg-zinc-800";
|
||||
return "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900";
|
||||
}
|
||||
|
||||
export default function WorktreeTaskIsolation({ title }: { title?: string }) {
|
||||
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2600 });
|
||||
const step = STEPS[vis.currentStep];
|
||||
|
||||
return (
|
||||
<section className="min-h-[500px] space-y-4">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title || "Worktree Task Isolation"}
|
||||
</h2>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="mb-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 font-mono text-xs text-blue-700 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-300">
|
||||
{step.op}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
|
||||
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
Task Board (.tasks)
|
||||
</div>
|
||||
<div className="space-y-2 p-2">
|
||||
{step.tasks.map((task) => (
|
||||
<motion.div
|
||||
key={`${task.id}-${task.status}-${task.worktree}`}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="rounded border border-zinc-200 p-2 text-xs dark:border-zinc-700"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-mono text-zinc-500 dark:text-zinc-400">#{task.id}</span>
|
||||
<span className={`rounded px-1.5 py-0.5 text-[10px] font-semibold ${statusClass(task.status)}`}>
|
||||
{task.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 font-medium text-zinc-800 dark:text-zinc-100">{task.subject}</div>
|
||||
<div className="mt-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400">
|
||||
worktree: {task.worktree || "-"}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
|
||||
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
Worktree Index (.worktrees/index.json)
|
||||
</div>
|
||||
<div className="space-y-2 p-2">
|
||||
{step.worktrees.length === 0 && (
|
||||
<div className="rounded border border-dashed border-zinc-300 px-3 py-4 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
|
||||
no worktrees yet
|
||||
</div>
|
||||
)}
|
||||
{step.worktrees.map((wt) => (
|
||||
<motion.div
|
||||
key={`${wt.name}-${wt.state}`}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className={`rounded border p-2 text-xs ${worktreeClass(wt.state)}`}
|
||||
>
|
||||
<div className="font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100">{wt.name}</div>
|
||||
<div className="font-mono text-[10px] text-zinc-500 dark:text-zinc-400">{wt.branch}</div>
|
||||
<div className="mt-1 text-[10px] text-zinc-600 dark:text-zinc-300">task: {wt.task}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
|
||||
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
Execution Lanes
|
||||
</div>
|
||||
<div className="space-y-2 p-2">
|
||||
{step.lanes.map((lane) => (
|
||||
<motion.div
|
||||
key={`${lane.name}-${lane.files.join(",")}`}
|
||||
initial={{ opacity: 0, x: -4 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className={`rounded border p-2 text-xs ${
|
||||
lane.highlight
|
||||
? "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20"
|
||||
: "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"
|
||||
}`}
|
||||
>
|
||||
<div className="font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100">{lane.name}</div>
|
||||
<div className="mt-1 space-y-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400">
|
||||
{lane.files.length === 0 ? (
|
||||
<div>(no changes)</div>
|
||||
) : (
|
||||
lane.files.map((f) => <div key={f}>{f}</div>)
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-800/60">
|
||||
<div className="font-medium text-zinc-800 dark:text-zinc-100">{step.title}</div>
|
||||
<div className="text-zinc-600 dark:text-zinc-300">{step.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StepControls
|
||||
currentStep={vis.currentStep}
|
||||
totalSteps={vis.totalSteps}
|
||||
onPrev={vis.prev}
|
||||
onNext={vis.next}
|
||||
onReset={vis.reset}
|
||||
isPlaying={vis.isPlaying}
|
||||
onToggleAutoPlay={vis.toggleAutoPlay}
|
||||
stepTitle={step.title}
|
||||
stepDescription={step.desc}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
289
web/src/components/visualizations/s15-team-runtime.tsx
Normal file
289
web/src/components/visualizations/s15-team-runtime.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ClipboardList,
|
||||
GitBranch,
|
||||
Inbox,
|
||||
LockKeyhole,
|
||||
Search,
|
||||
Terminal,
|
||||
UsersRound,
|
||||
} from "lucide-react";
|
||||
import { StepControls } from "@/components/visualizations/shared/step-controls";
|
||||
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
title: "Confirm a Small Team",
|
||||
desc: "The Lead proposes focused roles and waits for the user before starting persistent teammates.",
|
||||
event: "user confirmed: backend + tests",
|
||||
},
|
||||
{
|
||||
title: "Deliver a Typed Assignment",
|
||||
desc: "The runtime writes the assignment to a mailbox and correlates plan approval with a request id.",
|
||||
event: "plan_response(req_7, approved=true)",
|
||||
},
|
||||
{
|
||||
title: "Idle Teammates Scan the Board",
|
||||
desc: "A teammate with no direct message looks only for pending, unowned work whose dependencies are complete.",
|
||||
event: "scan_ready_tasks(backend) -> task_auth",
|
||||
},
|
||||
{
|
||||
title: "Claim Under One Lock",
|
||||
desc: "Ownership and status change atomically, so another teammate cannot take the same task.",
|
||||
event: "task_lock: task_auth -> backend",
|
||||
},
|
||||
{
|
||||
title: "Route Tools to the Task Directory",
|
||||
desc: "The claimed task carries its worktree binding; bash, read, and write derive their cwd from that record.",
|
||||
event: "cwd -> .worktrees/auth-refactor",
|
||||
},
|
||||
{
|
||||
title: "Return the Result, Keep the Teammate",
|
||||
desc: "The runtime delivers the result to the Lead and moves the teammate back to IDLE for newly ready work.",
|
||||
event: "result(auth complete) -> idle_notification",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const EVENTS = STEPS.map((step) => step.event);
|
||||
|
||||
function StateBadge({
|
||||
label,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
tone: "zinc" | "blue" | "amber" | "emerald";
|
||||
}) {
|
||||
const classes = {
|
||||
zinc: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300",
|
||||
blue: "bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200",
|
||||
amber: "bg-amber-100 text-amber-700 dark:bg-amber-950/50 dark:text-amber-200",
|
||||
emerald:
|
||||
"bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-200",
|
||||
}[tone];
|
||||
|
||||
return (
|
||||
<span className={cn("shrink-0 whitespace-nowrap rounded px-2 py-1 text-[11px] font-semibold", classes)}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimePanel({ step }: { step: number }) {
|
||||
const state =
|
||||
step === 0 ? "awaiting confirmation" : step === 1 ? "working" : step === 5 ? "idle" : "active";
|
||||
const tone = step === 0 ? "zinc" : step === 5 ? "emerald" : "blue";
|
||||
|
||||
return (
|
||||
<div className="min-h-[250px] rounded-lg border border-zinc-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<UsersRound size={16} className="shrink-0 text-blue-500" />
|
||||
<span className="break-words">Team runtime</span>
|
||||
</div>
|
||||
<StateBadge label={state} tone={tone} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-[88px_minmax(0,1fr)] gap-x-2 gap-y-3 text-xs">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Lead</span>
|
||||
<span className="break-words font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{step === 0 ? "proposes roles" : step === 5 ? "receives result" : "coordinates"}
|
||||
</span>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Teammate</span>
|
||||
<span className="break-words font-medium text-zinc-800 dark:text-zinc-200">
|
||||
backend
|
||||
</span>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Protocol</span>
|
||||
<span className="break-all font-mono text-zinc-700 dark:text-zinc-300">
|
||||
{step < 1 ? "-" : "request_id=req_7"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex min-h-[72px] items-start gap-2 rounded-md border border-blue-200 bg-blue-50 p-3 text-xs text-blue-800 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-200">
|
||||
<Inbox size={15} className="mt-0.5 shrink-0" />
|
||||
<span className="break-words leading-relaxed">
|
||||
{step === 0
|
||||
? "No mailbox is created before confirmation."
|
||||
: step === 1
|
||||
? "Assignment and plan approval travel through typed messages."
|
||||
: step === 5
|
||||
? "The result wakes the Lead; IDLE is a reusable state."
|
||||
: "The runtime owns delivery while the teammate works."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskPanel({ step }: { step: number }) {
|
||||
const status = step < 3 ? "pending" : step < 5 ? "in_progress" : "completed";
|
||||
const owner = step < 3 ? "-" : "backend";
|
||||
const tone = status === "pending" ? "zinc" : status === "in_progress" ? "amber" : "emerald";
|
||||
|
||||
return (
|
||||
<div className="min-h-[250px] rounded-lg border border-zinc-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<ClipboardList size={16} className="shrink-0 text-amber-500" />
|
||||
<span>Task board</span>
|
||||
</div>
|
||||
<StateBadge label={status} tone={tone} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-md border border-zinc-200 p-3 dark:border-zinc-700">
|
||||
<div className="font-mono text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
task_auth
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Refactor authentication
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-[76px_minmax(0,1fr)] gap-2 text-xs">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">owner</span>
|
||||
<span className="font-mono text-zinc-700 dark:text-zinc-300">{owner}</span>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">blockedBy</span>
|
||||
<span className="font-mono text-zinc-700 dark:text-zinc-300">[]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 flex min-h-[56px] items-center gap-2 rounded-md border px-3 py-2 text-xs",
|
||||
step === 2 || step === 3
|
||||
? "border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200"
|
||||
: "border-zinc-200 bg-zinc-50 text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
|
||||
)}
|
||||
>
|
||||
{step < 3 ? <Search size={15} className="shrink-0" /> : <LockKeyhole size={15} className="shrink-0" />}
|
||||
<span className="break-words">
|
||||
{step < 2
|
||||
? "Waiting for the teammate loop."
|
||||
: step === 2
|
||||
? "Ready filter: pending + unowned + dependencies complete."
|
||||
: "The claim check and update share one lock."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspacePanel({ step }: { step: number }) {
|
||||
const bound = step >= 4;
|
||||
|
||||
return (
|
||||
<div className="min-h-[250px] rounded-lg border border-zinc-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<GitBranch size={16} className="shrink-0 text-emerald-500" />
|
||||
<span>Task directory</span>
|
||||
</div>
|
||||
<StateBadge label={bound ? "bound" : "reserved"} tone={bound ? "emerald" : "zinc"} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md border p-3 transition-colors",
|
||||
!bound
|
||||
? "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30"
|
||||
: "border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
)}
|
||||
>
|
||||
<div className="font-mono text-xs font-semibold text-zinc-800 dark:text-zinc-200">
|
||||
repository root
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
coordination state
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
animate={bound ? { y: [0, -2, 0] } : { y: 0 }}
|
||||
transition={{ duration: 0.8, repeat: bound ? Infinity : 0 }}
|
||||
className={cn(
|
||||
"rounded-md border p-3 transition-colors",
|
||||
bound
|
||||
? "border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/30"
|
||||
: "border-dashed border-zinc-300 bg-white dark:border-zinc-700 dark:bg-zinc-900"
|
||||
)}
|
||||
>
|
||||
<div className="break-all font-mono text-xs font-semibold text-zinc-800 dark:text-zinc-200">
|
||||
.worktrees/auth-refactor
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
{bound ? "bash / read / write cwd" : "task.worktree binding"}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-300">
|
||||
{bound ? <Terminal size={15} /> : <GitBranch size={15} />}
|
||||
<span>{bound ? "Tools follow the claimed task." : "No implicit directory switching."}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamRuntime({ title }: { title?: string }) {
|
||||
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2800 });
|
||||
const step = vis.currentStep;
|
||||
const current = STEPS[step];
|
||||
|
||||
return (
|
||||
<section className="min-h-[500px] space-y-4">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title || "Agent Team Runtime"}
|
||||
</h2>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-700 dark:bg-zinc-950">
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
<RuntimePanel step={step} />
|
||||
<TaskPanel step={step} />
|
||||
<WorkspacePanel step={step} />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-lg border border-zinc-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||
<CheckCircle2 size={14} />
|
||||
Runtime events
|
||||
</div>
|
||||
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
|
||||
{EVENTS.map((event, index) => {
|
||||
const visible = index <= step;
|
||||
return (
|
||||
<motion.div
|
||||
key={event}
|
||||
initial={false}
|
||||
animate={{ opacity: visible ? 1 : 0.18, y: 0 }}
|
||||
aria-hidden={!visible}
|
||||
className={cn(
|
||||
"break-all rounded-md border px-2 py-1.5 font-mono text-[10px]",
|
||||
index === step
|
||||
? "border-blue-300 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950/30 dark:text-blue-200"
|
||||
: "border-zinc-200 bg-zinc-50 text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
|
||||
)}
|
||||
>
|
||||
{event}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StepControls
|
||||
className="mt-4"
|
||||
currentStep={vis.currentStep}
|
||||
totalSteps={vis.totalSteps}
|
||||
onPrev={vis.prev}
|
||||
onNext={vis.next}
|
||||
onReset={vis.reset}
|
||||
isPlaying={vis.isPlaying}
|
||||
onToggleAutoPlay={vis.toggleAutoPlay}
|
||||
stepTitle={current.title}
|
||||
stepDescription={current.desc}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ const STAGES: {
|
||||
{
|
||||
id: "execute",
|
||||
label: "Execute",
|
||||
detail: "local tools, teams, worktrees",
|
||||
detail: "local tools, teams, task-bound worktrees",
|
||||
icon: <Wrench size={15} />,
|
||||
},
|
||||
{
|
||||
@@ -82,7 +82,7 @@ const STAGES: {
|
||||
const SURFACES = [
|
||||
{ label: "background", icon: <Clock3 size={14} />, text: "slow commands can finish later" },
|
||||
{ label: "team", icon: <Network size={14} />, text: "teammates work through mailboxes" },
|
||||
{ label: "worktree", icon: <GitBranch size={14} />, text: "risky edits stay isolated" },
|
||||
{ label: "worktree", icon: <GitBranch size={14} />, text: "task-bound cwd selects a separate checkout" },
|
||||
{ label: "MCP", icon: <Blocks size={14} />, text: "external tools are normalized" },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user