"use client"; import { memo, useState, useEffect, useCallback, useMemo } from "react"; import { IconLoader2 } from "@tabler/icons-react"; import { PanelRoot, PanelBody } from "./panel-primitives"; import { FileDiffViewer } from "@/components/diff"; import { useAppStore } from "@/components/state-provider"; import { useSessionCommits } from "@/hooks/domains/session/use-session-commits"; import { useToast } from "@/components/toast-provider"; import { usePanelActions } from "@/hooks/use-panel-actions"; import { setPanelTitle } from "@/lib/layout/panel-portal-manager"; import type { FileInfo } from "@/lib/state/store"; import { requestCommitDiff } from "./commit-diff-request"; type CommitDetailPanelProps = { panelId: string; params: Record; }; function formatRelativeTime(dateStr: string): string { const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMin = Math.floor(diffMs / 60000); if (diffMin < 1) return "just now"; if (diffMin < 60) return `${diffMin} minute${diffMin !== 1 ? "s" : ""} ago`; const diffHours = Math.floor(diffMin / 60); if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`; const diffDays = Math.floor(diffHours / 24); if (diffDays < 30) return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; return date.toLocaleDateString(); } function getInitials(name: string): string { return name .split(/\s+/) .map((w) => w[0]) .filter(Boolean) .slice(0, 2) .join("") .toUpperCase(); } const CommitDetailPanel = memo(function CommitDetailPanel({ panelId, params, }: CommitDetailPanelProps) { const commitSha = params.commitSha as string; const repo = (params.repo as string | undefined) ?? undefined; const activeSessionId = useAppStore((state) => state.tasks.activeSessionId); const activeTaskId = useAppStore((state) => state.tasks.activeTaskId); const sessionTaskId = useAppStore((state) => activeSessionId ? state.taskSessions.items[activeSessionId]?.task_id : undefined, ); const agentctlReady = useAppStore((state) => activeSessionId ? state.sessionAgentctl.itemsBySessionId[activeSessionId]?.status === "ready" : false, ); const { commits } = useSessionCommits(activeSessionId ?? null); const { toast } = useToast(); const { openFile } = usePanelActions(); const [files, setFiles] = useState | null>(null); const [loading, setLoading] = useState(false); // Find commit metadata const commit = useMemo( () => commits.find((c) => c.commit_sha === commitSha), [commits, commitSha], ); // Update tab title via dockview API stored in portal manager useEffect(() => { if (commit) { const shortSha = commitSha.slice(0, 7); const msg = commit.commit_message.length > 30 ? commit.commit_message.slice(0, 30) + "..." : commit.commit_message; setPanelTitle(panelId, `${shortSha} ${msg}`); } }, [commit, commitSha, panelId]); // Fetch diff const fetchDiff = useCallback(async () => { if (!activeSessionId) return; setLoading(true); try { const response = await requestCommitDiff({ sessionId: activeSessionId, taskId: sessionTaskId ?? activeTaskId ?? null, commitSha, agentctlReady, repo, }); if (response?.success && response.files) { setFiles(response.files); } } catch (err) { toast({ title: "Failed to load commit diff", description: err instanceof Error ? err.message : "An unexpected error occurred", variant: "error", }); } finally { setLoading(false); } }, [activeSessionId, activeTaskId, agentctlReady, commitSha, repo, sessionTaskId, toast]); useEffect(() => { fetchDiff(); }, [fetchDiff]); const fileEntries = useMemo(() => { if (!files) return []; return Object.entries(files).sort(([a], [b]) => a.localeCompare(b)); }, [files]); if (loading) { return (
Loading commit...
); } return (
{commit && }
); }); /** Commit metadata header with author and message */ function CommitHeader({ commit, commitSha, }: { commit: { author_name: string; commit_message: string; committed_at: string }; commitSha: string; }) { return (
{getInitials(commit.author_name)}

{commit.commit_message}

{commit.author_name} · {formatRelativeTime(commit.committed_at)} · {commitSha.slice(0, 7)}

); } /** List of file diffs in a commit */ function CommitFileList({ fileEntries, loading, onOpenFile, baseRef, repo, }: { fileEntries: [string, FileInfo][]; loading: boolean; onOpenFile: (path: string) => void; baseRef: string; repo?: string; }) { if (fileEntries.length === 0 && !loading) { return (
No files in this commit
); } return ( <> {fileEntries.map(([path, file]) => (
{file.diff ? ( ) : (
{path} -- binary or empty diff
)}
))} ); } export { CommitDetailPanel };