import { useState, useEffect, useCallback } from 'react'; import { Check, X, RotateCcw, FileText, FilePlus, Trash2, RefreshCw } from 'lucide-react'; import type { PendingChange } from '@/api/types'; import { api } from '@/api/client'; interface Props { sessionId: string; onPendingChange: (cb: (change: PendingChange) => void) => () => void; } export function DiffPane({ sessionId, onPendingChange }: Props) { const [changes, setChanges] = useState([]); const [loading, setLoading] = useState(true); const [expandedId, setExpandedId] = useState(null); const fetchPending = useCallback(async () => { try { const result = await api.pending.list(sessionId); setChanges(result); } catch (err) { console.error('fetch pending failed:', err); } finally { setLoading(false); } }, [sessionId]); // Initial load useEffect(() => { fetchPending(); }, [fetchPending]); // Listen for WS pending change events useEffect(() => { const unsub = onPendingChange((change) => { setChanges((prev) => { const idx = prev.findIndex((c) => c.id === change.id); if (idx >= 0) { const next = [...prev]; next[idx] = change; return next; } return [...prev, change]; }); }); return unsub; }, [onPendingChange]); const pendingChanges = changes.filter((c) => c.status === 'pending'); const resolvedChanges = changes.filter((c) => c.status !== 'pending'); const handleApplyOne = async (id: string) => { try { await api.pending.applyOne(id); setChanges((prev) => prev.map((c) => (c.id === id ? { ...c, status: 'applied' as const } : c)), ); } catch (err) { console.error('apply failed:', err); } }; const handleRejectOne = async (id: string) => { try { await api.pending.rejectOne(id); setChanges((prev) => prev.map((c) => (c.id === id ? { ...c, status: 'rejected' as const } : c)), ); } catch (err) { console.error('reject failed:', err); } }; const handleRewindOne = async (id: string) => { try { await api.pending.rewindOne(id); setChanges((prev) => prev.map((c) => (c.id === id ? { ...c, status: 'reverted' as const } : c)), ); } catch (err) { console.error('rewind failed:', err); } }; const handleApplyAll = async () => { try { const result = await api.pending.applyAll(sessionId); const appliedIds = new Set( result.results.filter((r) => r.success).map((r) => r.id), ); setChanges((prev) => prev.map((c) => appliedIds.has(c.id) ? { ...c, status: 'applied' as const } : c, ), ); } catch (err) { console.error('apply all failed:', err); } }; const handleRejectAll = async () => { // Reject each pending change individually (no batch reject endpoint) for (const c of pendingChanges) { await handleRejectOne(c.id); } }; const OpIcon = ({ op }: { op: PendingChange['operation'] }) => { switch (op) { case 'create': return ; case 'edit': return ; case 'delete': return ; } }; const StatusBadge = ({ status }: { status: PendingChange['status'] }) => { const colors: Record = { pending: 'bg-yellow-500/20 text-yellow-400', applied: 'bg-green-500/20 text-green-400', rejected: 'bg-zinc-500/20 text-zinc-400', reverted: 'bg-orange-500/20 text-orange-400', }; return ( {status} ); }; return (
{/* Header */}

Pending Changes {pendingChanges.length > 0 && ( ({pendingChanges.length}) )}

{pendingChanges.length > 0 && ( <> )}
{/* Changes list */}
{loading && (
Loading...
)} {!loading && changes.length === 0 && (
No pending changes yet.
)} {/* Pending changes first */} {pendingChanges.map((change) => ( setExpandedId((prev) => (prev === change.id ? null : change.id)) } onApply={() => handleApplyOne(change.id)} onReject={() => handleRejectOne(change.id)} OpIcon={OpIcon} StatusBadge={StatusBadge} /> ))} {/* Resolved changes */} {resolvedChanges.length > 0 && pendingChanges.length > 0 && (
)} {resolvedChanges.map((change) => ( setExpandedId((prev) => (prev === change.id ? null : change.id)) } onRewind={ change.status === 'applied' ? () => handleRewindOne(change.id) : undefined } OpIcon={OpIcon} StatusBadge={StatusBadge} /> ))}
); } interface ChangeItemProps { change: PendingChange; expanded: boolean; onToggle: () => void; onApply?: () => void; onReject?: () => void; onRewind?: () => void; OpIcon: React.ComponentType<{ op: PendingChange['operation'] }>; StatusBadge: React.ComponentType<{ status: PendingChange['status'] }>; } function ChangeItem({ change, expanded, onToggle, onApply, onReject, onRewind, OpIcon, StatusBadge, }: ChangeItemProps) { const fileName = change.file_path.split('/').pop() || change.file_path; const dirPath = change.file_path.split('/').slice(0, -1).join('/'); return (
{fileName} {dirPath && ( {dirPath} )}
{change.status === 'pending' && (
)} {change.status === 'applied' && onRewind && ( )}
{expanded && (
{change.operation === 'edit' && (
{change.old_string && (
Remove
                    {change.old_string}
                  
)} {change.new_string && (
Add
                    {change.new_string}
                  
)}
)} {change.operation === 'create' && change.content && (
New file
                {change.content.length > 2000
                  ? change.content.slice(0, 2000) + '\n... (truncated)'
                  : change.content}
              
)} {change.operation === 'delete' && (
This file will be deleted.
)}
)}
); }