Consolidate agent harness course into 19 lessons

This commit is contained in:
Haoran
2026-08-04 02:25:40 +08:00
parent 2ad77cee19
commit b36dbcd84f
168 changed files with 6544 additions and 10400 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 }) {

View File

@@ -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} -&gt; {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>
);
}

View File

@@ -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.",
},
];

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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" },
];