agent-ecosystem/src/renderer/components/team/review/FileEditTimeline.tsx
iliya 190cafdb8e feat: implement diff view with 4 phases — review, accept/reject, task scoping, enhanced UX
Phase 1: Core diff extraction and display
- ChangeExtractorService: JSONL streaming parser with snippet extraction
- FileContentResolver: 3-level content resolution (file-history → snippets → disk)
- ReviewApplierService: hunk-level accept/reject with conflict detection
- CodeMirrorDiffView: unified merge view with syntax highlighting
- ReviewFileTree: file browser with status indicators
- changeReviewSlice: Zustand state for review workflow

Phase 2: Interactive review with accept/reject
- Per-hunk and per-file accept/reject decisions
- Conflict checking before apply
- ReviewToolbar with bulk actions
- DiffErrorBoundary for graceful degradation

Phase 3: Per-task change scoping
- TaskBoundaryParser: detects task boundaries in JSONL (Tier 1-4 confidence)
- TaskChangeSetV2 with scope + warnings
- ConfidenceBadge and ScopeWarningBanner components

Phase 4: Enhanced features
- Keyboard navigation (j/k/n/p/a/x shortcuts via useDiffNavigation)
- Viewed file tracking (localStorage + useViewedFiles hook)
- File edit timeline (chronological events per file)
- Git fallback (GitDiffFallback service for incomplete JSONL data)
- Auto-viewed detection (IntersectionObserver sentinel)
2026-02-24 23:39:41 +02:00

81 lines
2.6 KiB
TypeScript

import { cn } from '@renderer/lib/utils';
import type { FileEditTimeline as FileEditTimelineType } from '@shared/types/review';
interface FileEditTimelineProps {
timeline: FileEditTimelineType;
onEventClick?: (snippetIndex: number) => void;
activeSnippetIndex?: number;
}
export const FileEditTimeline = ({
timeline,
onEventClick,
activeSnippetIndex,
}: FileEditTimelineProps) => {
if (timeline.events.length === 0) {
return <div className="px-3 py-2 text-xs text-text-muted">No edit events</div>;
}
return (
<div className="space-y-0 px-3 py-2">
{timeline.events.map((event, idx) => {
const isActive = activeSnippetIndex === event.snippetIndex;
const isLast = idx === timeline.events.length - 1;
const time = formatTime(event.timestamp);
return (
<div key={`${event.toolUseId}-${idx}`} className="flex">
{/* Timeline line + dot */}
<div className="flex w-5 shrink-0 flex-col items-center">
<div
className={cn(
'mt-1.5 size-2 shrink-0 rounded-full',
isActive ? 'bg-blue-400' : 'bg-zinc-500'
)}
/>
{!isLast && <div className="w-px flex-1 bg-zinc-700" />}
</div>
{/* Content */}
<button
onClick={() => onEventClick?.(event.snippetIndex)}
className={cn(
'mb-1.5 flex w-full items-center gap-2 rounded px-1.5 py-1 text-left transition-colors',
isActive
? 'bg-blue-500/10 text-blue-300'
: 'text-text-secondary hover:bg-surface-raised hover:text-text'
)}
>
<span className="shrink-0 font-mono text-[10px] text-text-muted">{time}</span>
<span className="min-w-0 flex-1 truncate text-xs">{event.summary}</span>
<span className="flex shrink-0 items-center gap-0.5 text-[10px]">
{event.linesAdded > 0 && (
<span className="text-green-400">+{event.linesAdded}</span>
)}
{event.linesRemoved > 0 && (
<span className="text-red-400">-{event.linesRemoved}</span>
)}
</span>
</button>
</div>
);
})}
</div>
);
};
function formatTime(timestamp: string): string {
try {
const date = new Date(timestamp);
if (isNaN(date.getTime())) return '??:??';
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
} catch {
return '??:??';
}
}