import { useState } from "react";
import type { TaskCheckpoint } from "../../lib/api";
import { forkTask, resumeFromCheckpoint } from "../../lib/api";
interface CheckpointCardProps {
checkpoint: TaskCheckpoint;
taskId: string;
onActionComplete: () => void;
}
export function CheckpointCard({ checkpoint, taskId, onActionComplete }: CheckpointCardProps) {
const [showActions, setShowActions] = useState(false);
const [showForkDialog, setShowForkDialog] = useState(false);
const [showResumeDialog, setShowResumeDialog] = useState(false);
const [forkName, setForkName] = useState(`Fork from checkpoint ${checkpoint.checkpointNumber}`);
const [forkPlan, setForkPlan] = useState("");
const [resumePlan, setResumePlan] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleFork = async () => {
if (!forkName.trim() || !forkPlan.trim()) {
setError("Name and plan are required");
return;
}
setIsLoading(true);
setError(null);
try {
await forkTask(taskId, {
forkFromType: "checkpoint",
forkFromValue: String(checkpoint.checkpointNumber),
newTaskName: forkName,
newTaskPlan: forkPlan,
includeConversation: true,
});
setShowForkDialog(false);
onActionComplete();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to fork task");
} finally {
setIsLoading(false);
}
};
const handleResume = async () => {
if (!resumePlan.trim()) {
setError("Plan is required");
return;
}
setIsLoading(true);
setError(null);
try {
await resumeFromCheckpoint(taskId, checkpoint.id, {
plan: resumePlan,
includeConversation: true,
});
setShowResumeDialog(false);
onActionComplete();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to resume from checkpoint");
} finally {
setIsLoading(false);
}
};
return (
<>
<div className="p-3 hover:bg-[rgba(117,170,252,0.05)] transition-colors">
<div className="flex items-start justify-between gap-4">
{/* Checkpoint info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-purple-400">#{checkpoint.checkpointNumber}</span>
<span className="font-mono text-[10px] text-[#7788aa]">
{checkpoint.commitSha.slice(0, 7)}
</span>
<span className="font-mono text-[10px] text-[#556677]">
on {checkpoint.branchName}
</span>
</div>
{checkpoint.message && (
<div className="font-mono text-xs text-[#9bc3ff] mt-1">{checkpoint.message}</div>
)}
{/* Files changed */}
{checkpoint.filesChanged.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{checkpoint.filesChanged.slice(0, 5).map((file, i) => (
<span
key={i}
className={`font-mono text-[9px] px-1.5 py-0.5 border ${
file.action === "A"
? "text-green-400 border-green-400/30"
: file.action === "D"
? "text-red-400 border-red-400/30"
: "text-yellow-400 border-yellow-400/30"
}`}
>
{file.action} {file.path.split("/").pop()}
</span>
))}
{checkpoint.filesChanged.length > 5 && (
<span className="font-mono text-[9px] text-[#556677]">
+{checkpoint.filesChanged.length - 5} more
</span>
)}
</div>
)}
{/* Stats */}
<div className="mt-2 flex items-center gap-4 font-mono text-[9px] text-[#556677]">
<span className="text-green-400">+{checkpoint.linesAdded}</span>
<span className="text-red-400">-{checkpoint.linesRemoved}</span>
<span>{new Date(checkpoint.createdAt).toLocaleString()}</span>
</div>
</div>
{/* Actions button */}
<button
onClick={() => setShowActions(!showActions)}
className="shrink-0 p-1 text-[#7788aa] hover:text-[#9bc3ff] transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
</button>
</div>
{/* Actions dropdown */}
{showActions && (
<div className="mt-3 flex gap-2">
<button
onClick={() => {
setShowForkDialog(true);
setShowActions(false);
}}
className="px-3 py-1.5 font-mono text-[10px] uppercase text-purple-400 border border-purple-400/30 hover:bg-purple-400/10 transition-colors"
>
Fork from here
</button>
<button
onClick={() => {
setShowResumeDialog(true);
setShowActions(false);
}}
className="px-3 py-1.5 font-mono text-[10px] uppercase text-cyan-400 border border-cyan-400/30 hover:bg-cyan-400/10 transition-colors"
>
Resume from here
</button>
</div>
)}
</div>
{/* Fork dialog */}
{showForkDialog && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4">
<div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center">
<h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]">
Fork from Checkpoint #{checkpoint.checkpointNumber}
</h2>
<button
onClick={() => setShowForkDialog(false)}
className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-4 space-y-4">
{error && (
<div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400">
{error}
</div>
)}
<div>
<label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1">
New Task Name
</label>
<input
type="text"
value={forkName}
onChange={(e) => setForkName(e.target.value)}
className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none"
/>
</div>
<div>
<label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1">
Plan for New Task
</label>
<textarea
value={forkPlan}
onChange={(e) => setForkPlan(e.target.value)}
rows={4}
className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none resize-none"
placeholder="Describe what this forked task should accomplish..."
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowForkDialog(false)}
className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors"
>
Cancel
</button>
<button
onClick={handleFork}
disabled={isLoading}
className="px-4 py-2 font-mono text-xs uppercase text-white bg-purple-600 border border-purple-500 hover:bg-purple-500 transition-colors disabled:opacity-50"
>
{isLoading ? "Creating..." : "Create Fork"}
</button>
</div>
</div>
</div>
</div>
)}
{/* Resume dialog */}
{showResumeDialog && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4">
<div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center">
<h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]">
Resume from Checkpoint #{checkpoint.checkpointNumber}
</h2>
<button
onClick={() => setShowResumeDialog(false)}
className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-4 space-y-4">
{error && (
<div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400">
{error}
</div>
)}
<div>
<label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1">
Plan for Resumed Task
</label>
<textarea
value={resumePlan}
onChange={(e) => setResumePlan(e.target.value)}
rows={4}
className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none resize-none"
placeholder="Describe what the resumed task should do..."
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowResumeDialog(false)}
className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors"
>
Cancel
</button>
<button
onClick={handleResume}
disabled={isLoading}
className="px-4 py-2 font-mono text-xs uppercase text-white bg-cyan-600 border border-cyan-500 hover:bg-cyan-500 transition-colors disabled:opacity-50"
>
{isLoading ? "Creating..." : "Resume Task"}
</button>
</div>
</div>
</div>
</div>
)}
</>
);
}