"use client"; import { useState, useRef, useEffect } from "react"; import { Task, CommitRef } from "@/types"; import { useUpdateTask } from "@/hooks/use-tasks"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { GitCommit, GitBranch, ExternalLink, Clock, User, Plus, Trash2, X, Check } from "lucide-react"; import { toast } from "sonner"; import { getAgentDisplayName } from "@/lib/agent-utils"; interface TabCommitsProps { task: Task; } function formatTime(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffHours / 24); if (diffHours < 1) return "Just now"; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString("en-US", { month: "short", day: "numeric", }); } export function TabCommits({ task }: TabCommitsProps) { const updateTask = useUpdateTask(); const commits = task.commits; const [isAdding, setIsAdding] = useState(false); const [newHash, setNewHash] = useState(""); const [newMessage, setNewMessage] = useState(""); const hashRef = useRef(null); useEffect(() => { if (isAdding && hashRef.current) hashRef.current.focus(); }, [isAdding]); // Sort commits by timestamp (newest first) const sortedCommits = [...commits].sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); const handleAdd = async () => { if (!newHash.trim() || !newMessage.trim()) { if (!newHash.trim() && !newMessage.trim()) { setIsAdding(false); return; } toast.error("Both hash and message are required"); return; } // Check for duplicate hash if (commits.some((c) => c.hash === newHash.trim())) { toast.error("This commit hash is already linked"); return; } const newCommit: CommitRef = { hash: newHash.trim(), message: newMessage.trim(), timestamp: new Date().toISOString(), author_agent_id: "CEO", }; try { await updateTask.mutateAsync({ taskId: task.id, updates: { commits: [...commits, newCommit] }, }); setNewHash(""); setNewMessage(""); setIsAdding(false); } catch { toast.error("Failed to link commit"); } }; const handleDelete = async (hash: string) => { const newCommits = commits.filter((c) => c.hash !== hash); try { await updateTask.mutateAsync({ taskId: task.id, updates: { commits: newCommits }, }); } catch { toast.error("Failed to unlink commit"); } }; return (
Linked Commits ({commits.length})
{task.branch_name && ( {task.branch_name} )} {task.pr_url && ( PR #{task.pr_number} )}
{!isAdding && ( )}
{/* Add new commit form */} {isAdding && (
setNewHash(e.target.value)} placeholder="abc1234..." className="font-mono text-sm" />
setNewMessage(e.target.value)} placeholder="fix: resolved issue..." className="text-sm" />
)} {sortedCommits.length === 0 && !isAdding ? (

No commits linked to this task yet.

Commits will be linked as developers push code for this task.

) : (
{sortedCommits.map((commit) => (
{/* Commit message */}

{commit.message}

{/* Meta info */}
{/* Hash */} {commit.hash.slice(0, 7)} {/* Author */} {commit.author_agent_id && ( {getAgentDisplayName(commit.author_agent_id)} )} {/* Time */} {formatTime(commit.timestamp)}
))}
)} ); }