diff --git a/apps/arc-web/app/routes/run-files-changed.tsx b/apps/arc-web/app/routes/run-files-changed.tsx index 8fc399714..2d9c583d9 100644 --- a/apps/arc-web/app/routes/run-files-changed.tsx +++ b/apps/arc-web/app/routes/run-files-changed.tsx @@ -1,6 +1,10 @@ -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ChevronDownIcon, Cog6ToothIcon } from "@heroicons/react/24/outline"; -import { MultiFileDiff } from "@pierre/diffs/react"; +import { + MultiFileDiff, + type AnnotationSide, + type DiffLineAnnotation, +} from "@pierre/diffs/react"; export const handle = { wide: true }; @@ -234,8 +238,255 @@ function DiffStat({ additions, deletions }: { additions: number; deletions: numb ); } +interface SteerAnnotation { + fileName: string; + lineNumber: number; + side: AnnotationSide; +} + +function steerKey(fileName: string, side: AnnotationSide, lineNumber: number) { + return `${fileName}:${side}:${lineNumber}`; +} + +function DiffWithSteer({ + oldFile, + newFile, + openSteers, + submittedSteers, + onSteer, + onSubmit, + onCancel, +}: { + oldFile: { name: string; contents: string }; + newFile: { name: string; contents: string }; + openSteers: Map; + submittedSteers: Map; + onSteer: (annotation: SteerAnnotation) => void; + onSubmit: (annotation: SteerAnnotation, text: string) => void; + onCancel: (annotation: SteerAnnotation) => void; +}) { + const containerRef = useRef(null); + const buttonRef = useRef(null); + const hoveredRef = useRef<{ lineNumber: number; side: AnnotationSide } | null>(null); + const leaveTimeoutRef = useRef(0); + + const showButton = useCallback((lineElement: HTMLElement, lineNumber: number, side: AnnotationSide) => { + clearTimeout(leaveTimeoutRef.current); + hoveredRef.current = { lineNumber, side }; + + const btn = buttonRef.current; + const container = containerRef.current; + if (!btn || !container) return; + + const containerRect = container.getBoundingClientRect(); + const lineRect = lineElement.getBoundingClientRect(); + + btn.style.top = `${lineRect.top - containerRect.top}px`; + btn.style.height = `${lineRect.height}px`; + btn.style.display = "flex"; + }, []); + + const hideButton = useCallback(() => { + leaveTimeoutRef.current = window.setTimeout(() => { + hoveredRef.current = null; + if (buttonRef.current) { + buttonRef.current.style.display = "none"; + } + }, 100); + }, []); + + return ( +
+ + oldFile={oldFile} + newFile={newFile} + options={{ + diffStyle: "split", + theme: "pierre-dark", + lineDiffType: "word", + onLineEnter({ lineNumber, annotationSide, lineElement }) { + showButton(lineElement, lineNumber, annotationSide); + }, + onLineLeave() { + hideButton(); + }, + }} + lineAnnotations={buildAnnotationsForFile( + newFile.name, + openSteers, + submittedSteers, + )} + renderAnnotation={(annotation) => { + const meta = annotation.metadata; + if ("text" in meta && meta.text != null) { + return ; + } + return ( + onCancel(meta)} + /> + ); + }} + /> +
clearTimeout(leaveTimeoutRef.current)} + onMouseLeave={() => { + hoveredRef.current = null; + if (buttonRef.current) { + buttonRef.current.style.display = "none"; + } + }} + > + +
+
+ ); +} + +function SteerCommentForm({ + annotation, + onSubmit, + onCancel, +}: { + annotation: SteerAnnotation; + onSubmit: (annotation: SteerAnnotation, text: string) => void; + onCancel: () => void; +}) { + const [text, setText] = useState(""); + const textareaRef = useRef(null); + + useEffect(() => { + textareaRef.current?.focus(); + }, []); + + return ( +
+