@@ -111,6 +111,8 @@ export const PROverview = memo(function PROverview() {
111111 const [ workflowRunsAwaitingApproval , setWorkflowRunsAwaitingApproval ] =
112112 useState < Array < { id : number ; name : string ; html_url : string } > > ( [ ] ) ;
113113 const [ approvingWorkflows , setApprovingWorkflows ] = useState ( false ) ;
114+ // Track recently approved workflow IDs to filter out stale API responses
115+ const recentlyApprovedWorkflowIds = useRef < Set < number > > ( new Set ( ) ) ;
114116 const [ conversation , setConversation ] = useState < IssueComment [ ] > ( [ ] ) ;
115117 const [ commits , setCommits ] = useState < PRCommit [ ] > ( [ ] ) ;
116118 const [ timeline , setTimeline ] = useState < TimelineEvent [ ] > ( [ ] ) ;
@@ -204,16 +206,35 @@ export const PROverview = memo(function PROverview() {
204206 setChecksLastUpdated ( new Date ( ) ) ;
205207
206208 // Find workflow runs awaiting approval (fork PRs)
209+ // Filter out recently approved workflows to prevent stale API data from reverting optimistic updates
207210 const awaitingApproval = workflowRunsData . workflow_runs
208211 . filter (
209- ( run : { conclusion : string | null } ) =>
210- run . conclusion === "action_required"
212+ ( run : { id : number ; conclusion : string | null } ) =>
213+ run . conclusion === "action_required" &&
214+ ! recentlyApprovedWorkflowIds . current . has ( run . id )
211215 )
212216 . map ( ( run : { id : number ; name ?: string | null ; html_url : string } ) => ( {
213217 id : run . id ,
214218 name : run . name || "Workflow" ,
215219 html_url : run . html_url ,
216220 } ) ) ;
221+
222+ // Clear recently approved IDs that are no longer showing as action_required
223+ // (meaning GitHub's API has caught up)
224+ const currentActionRequiredIds = new Set (
225+ workflowRunsData . workflow_runs
226+ . filter (
227+ ( run : { conclusion : string | null } ) =>
228+ run . conclusion === "action_required"
229+ )
230+ . map ( ( run : { id : number } ) => run . id )
231+ ) ;
232+ for ( const id of recentlyApprovedWorkflowIds . current ) {
233+ if ( ! currentActionRequiredIds . has ( id ) ) {
234+ recentlyApprovedWorkflowIds . current . delete ( id ) ;
235+ }
236+ }
237+
217238 setWorkflowRunsAwaitingApproval ( awaitingApproval ) ;
218239 } catch {
219240 // Ignore errors on refresh
@@ -345,29 +366,51 @@ export const PROverview = memo(function PROverview() {
345366 merge_method : mergeMethod ,
346367 } ) ;
347368
348- window . location . reload ( ) ;
369+ // Invalidate caches for data that changes after merge
370+ // PR cache is already invalidated by mergePR, but also clear timeline
371+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } :timeline` ) ;
372+
373+ // Refetch the PR to get merged state and update the store
374+ const [ updatedPR , updatedTimeline ] = await Promise . all ( [
375+ github . getPR ( owner , repo , pr . number ) ,
376+ github
377+ . getPRTimeline ( owner , repo , pr . number )
378+ . catch ( ( ) => [ ] as TimelineEvent [ ] ) ,
379+ ] ) ;
380+
381+ store . setPr ( updatedPR ) ;
382+ setTimeline ( updatedTimeline ) ;
349383 } catch ( e ) {
350384 setMergeError ( e instanceof Error ? e . message : "Failed to merge" ) ;
351385 } finally {
352386 setMerging ( false ) ;
353387 }
354- } , [ github , owner , repo , pr . number , mergeMethod , track ] ) ;
388+ } , [ github , owner , repo , pr . number , mergeMethod , track , store ] ) ;
355389
356390 const handleApproveWorkflows = useCallback ( async ( ) => {
357391 setApprovingWorkflows ( true ) ;
358392 try {
393+ // Track which workflows we're approving to filter out stale API responses
394+ const approvedIds = workflowRunsAwaitingApproval . map ( ( run ) => run . id ) ;
395+ for ( const id of approvedIds ) {
396+ recentlyApprovedWorkflowIds . current . add ( id ) ;
397+ }
398+
399+ // Optimistically clear the UI immediately
400+ setWorkflowRunsAwaitingApproval ( [ ] ) ;
401+
359402 // Approve all workflow runs awaiting approval
360403 await Promise . all (
361- workflowRunsAwaitingApproval . map ( ( run ) =>
362- github . approveWorkflowRun ( owner , repo , run . id )
363- )
404+ approvedIds . map ( ( id ) => github . approveWorkflowRun ( owner , repo , id ) )
364405 ) ;
365- // Clear the list after approving
366- setWorkflowRunsAwaitingApproval ( [ ] ) ;
367- // Refresh checks to get updated status
406+
407+ // Refresh checks to get updated status (recently approved IDs will be filtered out)
368408 await fetchChecks ( ) ;
369409 } catch ( e ) {
370410 console . error ( "Failed to approve workflows:" , e ) ;
411+ // On error, clear the recently approved tracking and re-fetch to restore actual state
412+ recentlyApprovedWorkflowIds . current . clear ( ) ;
413+ await fetchChecks ( ) ;
371414 } finally {
372415 setApprovingWorkflows ( false ) ;
373416 }
@@ -923,12 +966,33 @@ export const PROverview = memo(function PROverview() {
923966 // Build unified timeline
924967 type TimelineEntry =
925968 | { type : "comment" ; data : IssueComment ; date : Date }
926- | { type : "review" ; data : Review ; date : Date }
969+ | {
970+ type : "review" ;
971+ data : Review ;
972+ threads : ReviewThread [ ] ;
973+ date : Date ;
974+ }
927975 | { type : "event" ; data : TimelineEvent ; date : Date }
928976 | { type : "thread" ; data : ReviewThread ; date : Date } ;
929977
930978 const entries : TimelineEntry [ ] = [ ] ;
931979
980+ // Build a map of review database ID -> threads that belong to it
981+ const threadsByReviewId = new Map < number , ReviewThread [ ] > ( ) ;
982+ const orphanedThreads : ReviewThread [ ] = [ ] ;
983+
984+ reviewThreads . forEach ( ( thread ) => {
985+ const reviewId = thread . pullRequestReview ?. databaseId ;
986+ if ( reviewId ) {
987+ const existing = threadsByReviewId . get ( reviewId ) || [ ] ;
988+ existing . push ( thread ) ;
989+ threadsByReviewId . set ( reviewId , existing ) ;
990+ } else {
991+ // Thread without associated review (shouldn't happen often)
992+ orphanedThreads . push ( thread ) ;
993+ }
994+ } ) ;
995+
932996 // Add comments
933997 conversation . forEach ( ( comment ) => {
934998 entries . push ( {
@@ -938,26 +1002,31 @@ export const PROverview = memo(function PROverview() {
9381002 } ) ;
9391003 } ) ;
9401004
941- // Add ALL reviews to timeline - show APPROVED/CHANGES_REQUESTED always, COMMENTED only if they have a body
942- // Note: we use `reviews` not `latestReviews` because latestReviews only keeps one review per user
1005+ // Add ALL reviews to timeline with their associated threads
1006+ // Show APPROVED/CHANGES_REQUESTED always, COMMENTED only if they have a body OR associated threads
9431007 reviews
944- . filter (
945- ( r ) =>
946- r . submitted_at &&
947- ( r . body ||
948- r . state === "APPROVED" ||
949- r . state === "CHANGES_REQUESTED" )
950- )
1008+ . filter ( ( r ) => {
1009+ if ( ! r . submitted_at ) return false ;
1010+ const hasThreads =
1011+ ( threadsByReviewId . get ( r . id ) ?. length ?? 0 ) > 0 ;
1012+ return (
1013+ r . body ||
1014+ r . state === "APPROVED" ||
1015+ r . state === "CHANGES_REQUESTED" ||
1016+ hasThreads
1017+ ) ;
1018+ } )
9511019 . forEach ( ( review ) => {
9521020 entries . push ( {
9531021 type : "review" ,
9541022 data : review ,
1023+ threads : threadsByReviewId . get ( review . id ) || [ ] ,
9551024 date : new Date ( review . submitted_at ! ) ,
9561025 } ) ;
9571026 } ) ;
9581027
959- // Add review threads (inline code comments )
960- reviewThreads . forEach ( ( thread ) => {
1028+ // Add orphaned threads (threads without a matching review in our list )
1029+ orphanedThreads . forEach ( ( thread ) => {
9611030 const firstComment = thread . comments . nodes [ 0 ] ;
9621031 if ( firstComment ) {
9631032 entries . push ( {
@@ -1041,10 +1110,23 @@ export const PROverview = memo(function PROverview() {
10411110 }
10421111 if ( entry . type === "review" ) {
10431112 return (
1044- < ReviewBox
1045- key = { `review-${ entry . data . id } ` }
1046- review = { entry . data }
1047- />
1113+ < div key = { `review-${ entry . data . id } ` } >
1114+ < ReviewBox review = { entry . data } />
1115+ { /* Render associated threads under the review */ }
1116+ { entry . threads . map ( ( thread ) => (
1117+ < ReviewThreadBox
1118+ key = { `thread-${ thread . id } ` }
1119+ thread = { thread }
1120+ owner = { owner }
1121+ repo = { repo }
1122+ onReply = { handleReplyToThread }
1123+ onResolve = { handleResolveThread }
1124+ onUnresolve = { handleUnresolveThread }
1125+ canWrite = { canWrite }
1126+ currentUser = { currentUser }
1127+ />
1128+ ) ) }
1129+ </ div >
10481130 ) ;
10491131 }
10501132 if ( entry . type === "event" ) {
@@ -1057,6 +1139,7 @@ export const PROverview = memo(function PROverview() {
10571139 ) ;
10581140 }
10591141 if ( entry . type === "thread" ) {
1142+ // Orphaned thread (no associated review)
10601143 return (
10611144 < ReviewThreadBox
10621145 key = { `thread-${ entry . data . id } ` }
@@ -2782,7 +2865,9 @@ function MergeSection({
27822865 alt = { review . user ?. login }
27832866 className = "w-5 h-5 rounded-full"
27842867 />
2785- < span className = "text-sm" > { review . user ?. login } </ span >
2868+ < span className = "text-sm" >
2869+ { review . user ?. login ?? "" }
2870+ </ span >
27862871 < ReviewStateIcon state = { review . state } showTooltip />
27872872 </ div >
27882873 ) ) }
0 commit comments