@@ -5,18 +5,18 @@ import type { DiffRow } from "./pierre";
55
66const EMPTY_VISIBLE_AGENT_NOTES : VisibleAgentNote [ ] = [ ] ;
77
8- interface SelectedInlineNote {
8+ type DiffLineRow = Extract < DiffRow , { type : "split-line" | "stack-line" } > ;
9+
10+ interface PrimaryVisibleInlineNote {
911 anchorKey : string ;
1012 anchorSide ?: "old" | "new" ;
11- coveredRowKeys : Set < string > ;
12- endGuideAfterKey : string ;
13+ guidedRowKeys : Set < string > ;
14+ endGuideAfterKey ? : string ;
1315 note : VisibleAgentNote ;
1416 noteCount : number ;
1517 noteIndex : number ;
1618}
1719
18- type DiffLineRow = Extract < DiffRow , { type : "split-line" | "stack-line" } > ;
19-
2020export type PlannedReviewRow =
2121 | {
2222 kind : "diff-row" ;
@@ -46,6 +46,16 @@ export type PlannedReviewRow =
4646 side : "old" | "new" ;
4747 } ;
4848
49+ function hunkRows ( rows : DiffRow [ ] , hunkIndex : number ) {
50+ return rows . filter ( ( row ) => row . hunkIndex === hunkIndex ) ;
51+ }
52+
53+ function hunkLineRows ( rows : DiffRow [ ] , hunkIndex : number ) {
54+ return hunkRows ( rows , hunkIndex ) . filter (
55+ ( row ) : row is DiffLineRow => row . type === "split-line" || row . type === "stack-line" ,
56+ ) ;
57+ }
58+
4959/** Check whether a rendered diff row visually covers the note anchor line. */
5060function rowMatchesNote ( row : DiffLineRow , annotation : AgentAnnotation ) {
5161 const anchor = annotationAnchor ( annotation ) ;
@@ -92,62 +102,81 @@ function rowOverlapsAnnotation(row: DiffLineRow, annotation: AgentAnnotation) {
92102 ) ;
93103}
94104
95- /** Resolve the rendered diff row before which the visible inline note should appear. */
105+ /**
106+ * Resolve the rendered diff row before which the inline note should appear.
107+ * Range-less notes intentionally anchor beside the first code row in the hunk,
108+ * not above the hunk header metadata.
109+ */
96110function findInlineNoteAnchorRow (
97111 rows : DiffRow [ ] ,
98112 annotation : AgentAnnotation ,
99113 selectedHunkIndex : number ,
100114) {
101- const selectedHunkRows = rows . filter ( ( row ) => row . hunkIndex === selectedHunkIndex ) ;
102- const lineRows = selectedHunkRows . filter (
103- ( row ) : row is DiffLineRow => row . type === "split-line" || row . type === "stack-line" ,
104- ) ;
115+ const selectedHunkRows = hunkRows ( rows , selectedHunkIndex ) ;
116+ const lineRows = hunkLineRows ( rows , selectedHunkIndex ) ;
105117 const headerRow = selectedHunkRows . find ( ( row ) => row . type === "hunk-header" ) ;
106118
107119 return lineRows . find ( ( row ) => rowMatchesNote ( row , annotation ) ) ?? lineRows [ 0 ] ?? headerRow ;
108120}
109121
110- /** Return the one visible note, plus the diff rows that should show its guide rail. */
111- function buildSelectedInlineNote (
122+ /**
123+ * The render plan shows at most one inline note at a time.
124+ * The first entry in visibleAgentNotes is the primary visible note, while
125+ * noteIndex/noteCount preserve its position within the current visible list.
126+ */
127+ function selectPrimaryVisibleNote ( visibleAgentNotes : VisibleAgentNote [ ] ) {
128+ if ( visibleAgentNotes . length === 0 ) {
129+ return null ;
130+ }
131+
132+ return {
133+ note : visibleAgentNotes [ 0 ] ! ,
134+ noteIndex : 0 ,
135+ noteCount : visibleAgentNotes . length ,
136+ } ;
137+ }
138+
139+ /** Return the primary visible note, plus the diff rows that should show its guide rail. */
140+ function buildPrimaryVisibleInlineNote (
112141 rows : DiffRow [ ] ,
113142 visibleAgentNotes : VisibleAgentNote [ ] ,
114143 selectedHunkIndex : number ,
115144) {
116- if ( visibleAgentNotes . length === 0 || selectedHunkIndex < 0 ) {
145+ if ( selectedHunkIndex < 0 ) {
117146 return null ;
118147 }
119148
120- const note = visibleAgentNotes [ 0 ] ! ;
121- const anchorRow = findInlineNoteAnchorRow ( rows , note . annotation , selectedHunkIndex ) ;
149+ const selectedNote = selectPrimaryVisibleNote ( visibleAgentNotes ) ;
150+ if ( ! selectedNote ) {
151+ return null ;
152+ }
153+
154+ const anchorRow = findInlineNoteAnchorRow ( rows , selectedNote . note . annotation , selectedHunkIndex ) ;
122155 if ( ! anchorRow ) {
123156 return null ;
124157 }
125158
126- const selectedHunkLineRows = rows . filter (
127- ( row ) : row is DiffLineRow =>
128- row . hunkIndex === selectedHunkIndex &&
129- ( row . type === "split-line" || row . type === "stack-line" ) ,
130- ) ;
159+ const selectedHunkLineRows = hunkLineRows ( rows , selectedHunkIndex ) ;
160+ const anchorSide = annotationAnchor ( selectedNote . note . annotation ) ?. side ;
131161 const coveredRows = selectedHunkLineRows . filter ( ( row ) =>
132- rowOverlapsAnnotation ( row , note . annotation ) ,
162+ rowOverlapsAnnotation ( row , selectedNote . note . annotation ) ,
133163 ) ;
134164 const fallbackGuideRow =
135- anchorRow . type === "split-line" || anchorRow . type === "stack-line"
165+ anchorSide && ( anchorRow . type === "split-line" || anchorRow . type === "stack-line" )
136166 ? anchorRow
137- : selectedHunkLineRows [ 0 ] ;
167+ : undefined ;
138168 const guideRows =
139169 coveredRows . length > 0 ? coveredRows : fallbackGuideRow ? [ fallbackGuideRow ] : [ ] ;
140- const endGuideAfterKey = guideRows . at ( - 1 ) ?. key ?? anchorRow . key ;
141170
142171 return {
143172 anchorKey : anchorRow . key ,
144- anchorSide : annotationAnchor ( note . annotation ) ?. side ,
145- coveredRowKeys : new Set ( guideRows . map ( ( row ) => row . key ) ) ,
146- endGuideAfterKey,
147- note,
148- noteIndex : 0 ,
149- noteCount : visibleAgentNotes . length ,
150- } satisfies SelectedInlineNote ;
173+ anchorSide,
174+ guidedRowKeys : new Set ( guideRows . map ( ( row ) => row . key ) ) ,
175+ endGuideAfterKey : guideRows . at ( - 1 ) ?. key ,
176+ note : selectedNote . note ,
177+ noteIndex : selectedNote . noteIndex ,
178+ noteCount : selectedNote . noteCount ,
179+ } satisfies PrimaryVisibleInlineNote ;
151180}
152181
153182function rowCanAnchorHunk ( row : DiffRow , showHunkHeaders : boolean ) {
@@ -158,7 +187,11 @@ function rowCanAnchorHunk(row: DiffRow, showHunkHeaders: boolean) {
158187 return row . type !== "collapsed" && row . type !== "hunk-header" ;
159188}
160189
161- /** Build the explicit presentational row plan for one file diff body. */
190+ /**
191+ * Build the explicit presentational row plan for one file diff body.
192+ * The plan always preserves diff-row order and may insert one inline note plus
193+ * one trailing guide cap for the primary visible note.
194+ */
162195export function buildReviewRenderPlan ( {
163196 fileId,
164197 rows,
@@ -172,7 +205,11 @@ export function buildReviewRenderPlan({
172205 showHunkHeaders : boolean ;
173206 visibleAgentNotes ?: VisibleAgentNote [ ] ;
174207} ) {
175- const selectedInlineNote = buildSelectedInlineNote ( rows , visibleAgentNotes , selectedHunkIndex ) ;
208+ const primaryVisibleNote = buildPrimaryVisibleInlineNote (
209+ rows ,
210+ visibleAgentNotes ,
211+ selectedHunkIndex ,
212+ ) ;
176213 const plannedRows : PlannedReviewRow [ ] = [ ] ;
177214 const anchoredHunks = new Set < number > ( ) ;
178215
@@ -185,17 +222,17 @@ export function buildReviewRenderPlan({
185222 anchoredHunks . add ( row . hunkIndex ) ;
186223 }
187224
188- if ( selectedInlineNote ?. anchorKey === row . key ) {
225+ if ( primaryVisibleNote ?. anchorKey === row . key ) {
189226 plannedRows . push ( {
190227 kind : "inline-note" ,
191- key : `inline-note:${ selectedInlineNote . note . id } :${ row . key } ` ,
228+ key : `inline-note:${ primaryVisibleNote . note . id } :${ row . key } ` ,
192229 fileId,
193230 hunkIndex : row . hunkIndex ,
194- annotationId : selectedInlineNote . note . id ,
195- annotation : selectedInlineNote . note . annotation ,
196- anchorSide : selectedInlineNote . anchorSide ,
197- noteCount : selectedInlineNote . noteCount ,
198- noteIndex : selectedInlineNote . noteIndex ,
231+ annotationId : primaryVisibleNote . note . id ,
232+ annotation : primaryVisibleNote . note . annotation ,
233+ anchorSide : primaryVisibleNote . anchorSide ,
234+ noteCount : primaryVisibleNote . noteCount ,
235+ noteIndex : primaryVisibleNote . noteIndex ,
199236 } ) ;
200237 }
201238
@@ -206,18 +243,18 @@ export function buildReviewRenderPlan({
206243 hunkIndex : row . hunkIndex ,
207244 row,
208245 anchorId,
209- noteGuideSide : selectedInlineNote ?. coveredRowKeys . has ( row . key )
210- ? selectedInlineNote . anchorSide
246+ noteGuideSide : primaryVisibleNote ?. guidedRowKeys . has ( row . key )
247+ ? primaryVisibleNote . anchorSide
211248 : undefined ,
212249 } ) ;
213250
214- if ( selectedInlineNote ?. endGuideAfterKey === row . key && selectedInlineNote . anchorSide ) {
251+ if ( primaryVisibleNote ?. anchorSide && primaryVisibleNote . endGuideAfterKey === row . key ) {
215252 plannedRows . push ( {
216253 kind : "note-guide-cap" ,
217- key : `note-guide-cap:${ selectedInlineNote . note . id } :${ row . key } ` ,
254+ key : `note-guide-cap:${ primaryVisibleNote . note . id } :${ row . key } ` ,
218255 fileId,
219256 hunkIndex : row . hunkIndex ,
220- side : selectedInlineNote . anchorSide ,
257+ side : primaryVisibleNote . anchorSide ,
221258 } ) ;
222259 }
223260 }
0 commit comments