11import type { ScrollBoxRenderable } from "@opentui/core" ;
22import { useCallback , useEffect , useLayoutEffect , useMemo , useState , type RefObject } from "react" ;
33import type { AgentAnnotation , DiffFile , LayoutMode } from "../../../core/types" ;
4- import type { VisibleAgentNote } from "../../lib/agentAnnotations" ;
4+ import { AgentCard } from "./AgentCard" ;
5+ import { annotationLocationLabel , type VisibleAgentNote } from "../../lib/agentAnnotations" ;
6+ import { buildAgentPopoverContent , resolveAgentPopoverPlacement } from "../../lib/agentPopover" ;
57import { estimateDiffBodyRows , estimateHunkAnchorRow } from "../../lib/sectionHeights" ;
68import { diffHunkId , diffSectionId } from "../../lib/ids" ;
79import type { AppTheme } from "../../themes" ;
@@ -10,6 +12,24 @@ import { DiffSectionPlaceholder } from "./DiffSectionPlaceholder";
1012
1113const EMPTY_VISIBLE_AGENT_NOTES : VisibleAgentNote [ ] = [ ] ;
1214
15+ function maxLineNumber ( file : DiffFile ) {
16+ return Math . max ( file . metadata . additionLines . length , file . metadata . deletionLines . length , 0 ) ;
17+ }
18+
19+ function noteAnchorColumn (
20+ file : DiffFile ,
21+ layout : Exclude < LayoutMode , "auto" > ,
22+ width : number ,
23+ showLineNumbers : boolean ,
24+ note : VisibleAgentNote ,
25+ ) {
26+ if ( layout === "split" ) {
27+ return note . annotation . oldRange && ! note . annotation . newRange ? 1 : Math . max ( 2 , Math . floor ( width * 0.58 ) ) ;
28+ }
29+
30+ return showLineNumbers ? Math . max ( 2 , String ( maxLineNumber ( file ) ) . length + 4 ) : 2 ;
31+ }
32+
1333/** Render the main multi-file review stream. */
1434export function DiffPane ( {
1535 activeAnnotations,
@@ -144,6 +164,65 @@ export function DiffPane({
144164 ( ) => files . map ( ( file ) => estimateDiffBodyRows ( file , layout , showHunkHeaders ) ) ,
145165 [ files , layout , showHunkHeaders ] ,
146166 ) ;
167+ const selectedOverlayNote = useMemo ( ( ) => {
168+ if ( ! selectedFileId ) {
169+ return null ;
170+ }
171+
172+ const selectedFileIndex = files . findIndex ( ( file ) => file . id === selectedFileId ) ;
173+ if ( selectedFileIndex < 0 ) {
174+ return null ;
175+ }
176+
177+ const selectedFile = files [ selectedFileIndex ] ! ;
178+ const visibleNotes = visibleAgentNotesByFile . get ( selectedFileId ) ?? EMPTY_VISIBLE_AGENT_NOTES ;
179+ const note = visibleNotes [ 0 ] ;
180+ if ( ! note ) {
181+ return null ;
182+ }
183+
184+ let sectionTop = 0 ;
185+ for ( let index = 0 ; index < selectedFileIndex ; index += 1 ) {
186+ sectionTop += ( index > 0 ? 1 : 0 ) + 1 + ( estimatedBodyHeights [ index ] ?? 0 ) ;
187+ }
188+
189+ sectionTop += ( selectedFileIndex > 0 ? 1 : 0 ) + 1 ;
190+ const anchorRowTop = sectionTop + estimateHunkAnchorRow ( selectedFile , layout , showHunkHeaders , selectedHunkIndex ) ;
191+ const anchorColumn = noteAnchorColumn ( selectedFile , layout , diffContentWidth , showLineNumbers , note ) ;
192+ const noteWidth = Math . min ( Math . max ( 34 , Math . floor ( diffContentWidth * 0.42 ) ) , Math . max ( 12 , diffContentWidth - 2 ) ) ;
193+ const locationLabel = annotationLocationLabel ( selectedFile , note . annotation ) ;
194+ const popover = buildAgentPopoverContent ( {
195+ summary : note . annotation . summary ,
196+ rationale : note . annotation . rationale ,
197+ locationLabel,
198+ noteIndex : 0 ,
199+ noteCount : visibleNotes . length ,
200+ width : noteWidth ,
201+ } ) ;
202+
203+ const contentHeight = files . reduce (
204+ ( total , file , index ) => total + ( index > 0 ? 1 : 0 ) + 1 + ( estimatedBodyHeights [ index ] ?? 0 ) ,
205+ 0 ,
206+ ) ;
207+ const placement = resolveAgentPopoverPlacement ( {
208+ anchorColumn,
209+ anchorRowTop,
210+ anchorRowHeight : 1 ,
211+ contentHeight,
212+ noteHeight : popover . height ,
213+ noteWidth,
214+ viewportWidth : diffContentWidth ,
215+ } ) ;
216+
217+ return {
218+ note,
219+ noteCount : visibleNotes . length ,
220+ noteWidth,
221+ left : placement . left ,
222+ top : placement . top ,
223+ locationLabel,
224+ } ;
225+ } , [ diffContentWidth , estimatedBodyHeights , files , layout , selectedFileId , selectedHunkIndex , showHunkHeaders , showLineNumbers , visibleAgentNotesByFile ] ) ;
147226
148227 const visibleViewportFileIds = useMemo ( ( ) => {
149228 const overscanRows = 8 ;
@@ -267,7 +346,7 @@ export function DiffPane({
267346 verticalScrollbarOptions = { { visible : false } }
268347 horizontalScrollbarOptions = { { visible : false } }
269348 >
270- < box style = { { width : "100%" , flexDirection : "column" } } >
349+ < box style = { { width : "100%" , flexDirection : "column" , position : "relative" , overflow : "visible" } } >
271350 { files . map ( ( file , index ) => {
272351 const shouldRenderSection = visibleWindowedFileIds ?. has ( file . id ) ?? true ;
273352 const shouldPrefetchVisibleHighlight =
@@ -295,7 +374,7 @@ export function DiffPane({
295374 wrapLines = { wrapLines }
296375 theme = { theme }
297376 viewWidth = { diffContentWidth }
298- visibleAgentNotes = { visibleAgentNotesByFile . get ( file . id ) ?? EMPTY_VISIBLE_AGENT_NOTES }
377+ visibleAgentNotes = { EMPTY_VISIBLE_AGENT_NOTES }
299378 onDismissAgentNote = { onDismissAgentNote }
300379 onOpenAgentNotesAtHunk = { ( hunkIndex ) => onOpenAgentNotesAtHunk ( file . id , hunkIndex ) }
301380 onSelect = { ( ) => onSelectFile ( file . id ) }
@@ -314,6 +393,19 @@ export function DiffPane({
314393 />
315394 ) ;
316395 } ) }
396+ { selectedFileId && selectedOverlayNote ? (
397+ < box style = { { position : "absolute" , top : selectedOverlayNote . top , left : selectedOverlayNote . left , zIndex : 20 } } >
398+ < AgentCard
399+ locationLabel = { selectedOverlayNote . locationLabel }
400+ noteCount = { selectedOverlayNote . noteCount }
401+ rationale = { selectedOverlayNote . note . annotation . rationale }
402+ summary = { selectedOverlayNote . note . annotation . summary }
403+ theme = { theme }
404+ width = { selectedOverlayNote . noteWidth }
405+ onClose = { ( ) => onDismissAgentNote ( selectedOverlayNote . note . id ) }
406+ />
407+ </ box >
408+ ) : null }
317409 </ box >
318410 </ scrollbox >
319411 ) : (
0 commit comments