@@ -103,7 +103,9 @@ export function App({
103103 const diffScrollRef = useRef < ScrollBoxRenderable | null > ( null ) ;
104104 const wrapToggleScrollTopRef = useRef < number | null > ( null ) ;
105105 const layoutToggleScrollTopRef = useRef < number | null > ( null ) ;
106+ const cancelCopySelectionRef = useRef < ( ( ) => void ) | null > ( null ) ;
106107 const [ layoutToggleRequestId , setLayoutToggleRequestId ] = useState ( 0 ) ;
108+ const [ transientNoticeText , setTransientNoticeText ] = useState < string | null > ( null ) ;
107109 const [ layoutMode , setLayoutMode ] = useState < LayoutMode > ( bootstrap . initialMode ) ;
108110 const [ themeId , setThemeId ] = useState ( ( ) =>
109111 bootstrap . initialTheme === "auto"
@@ -115,6 +117,7 @@ export function App({
115117 const [ showAgentNotes , setShowAgentNotes ] = useState ( bootstrap . initialShowAgentNotes ?? false ) ;
116118 const [ showLineNumbers , setShowLineNumbers ] = useState ( bootstrap . initialShowLineNumbers ?? true ) ;
117119 const [ wrapLines , setWrapLines ] = useState ( bootstrap . initialWrapLines ?? false ) ;
120+ const [ copyDecorations , setCopyDecorations ] = useState ( bootstrap . initialCopyDecorations ?? false ) ;
118121 const [ codeHorizontalOffset , setCodeHorizontalOffset ] = useState ( 0 ) ;
119122 const [ showHunkHeaders , setShowHunkHeaders ] = useState ( bootstrap . initialShowHunkHeaders ?? true ) ;
120123 const [ sidebarVisible , setSidebarVisible ] = useState ( ( ) => ! pagerMode ) ;
@@ -325,6 +328,35 @@ export function App({
325328 setShowLineNumbers ( ( current ) => ! current ) ;
326329 } ;
327330
331+ /** Toggle whether mouse selection copies review decorations or only file content. */
332+ const toggleCopyDecorations = ( ) => {
333+ setCopyDecorations ( ( current ) => ! current ) ;
334+ } ;
335+
336+ // Show a short-lived status-bar message. Used to surface clipboard-copy outcomes that would
337+ // otherwise be invisible to the user (OSC52 unsupported, etc.).
338+ // Track the timer so we can clear it on unmount and avoid React state updates after unmount.
339+ const transientTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
340+ const showTransientNotice = useCallback ( ( text : string , durationMs = 3000 ) => {
341+ if ( transientTimerRef . current !== null ) {
342+ clearTimeout ( transientTimerRef . current ) ;
343+ }
344+ setTransientNoticeText ( text ) ;
345+ transientTimerRef . current = setTimeout ( ( ) => {
346+ transientTimerRef . current = null ;
347+ setTransientNoticeText ( ( current ) => ( current === text ? null : current ) ) ;
348+ } , durationMs ) ;
349+ } , [ ] ) ;
350+
351+ // Clear any pending transient-notice timer on unmount to avoid state updates after unmount.
352+ useEffect ( ( ) => {
353+ return ( ) => {
354+ if ( transientTimerRef . current !== null ) {
355+ clearTimeout ( transientTimerRef . current ) ;
356+ }
357+ } ;
358+ } , [ ] ) ;
359+
328360 /** Toggle whether diff code rows wrap instead of truncating to one terminal row. */
329361 const toggleLineWrap = ( ) => {
330362 // Capture the pre-toggle viewport position synchronously so DiffPane can restore the same
@@ -569,11 +601,13 @@ export function App({
569601 requestQuit,
570602 selectLayoutMode,
571603 selectThemeId : setThemeId ,
604+ copyDecorations,
572605 showAgentNotes,
573606 showHelp,
574607 showHunkHeaders,
575608 showLineNumbers,
576609 renderSidebar,
610+ toggleCopyDecorations,
577611 toggleAgentNotes,
578612 toggleFocusArea,
579613 toggleHelp,
@@ -587,6 +621,7 @@ export function App({
587621 [
588622 activeTheme . id ,
589623 canRefreshCurrentInput ,
624+ copyDecorations ,
590625 focusFilter ,
591626 layoutMode ,
592627 moveToAnnotatedFile ,
@@ -595,6 +630,7 @@ export function App({
595630 review . moveToHunk ,
596631 selectLayoutMode ,
597632 triggerRefreshCurrentInput ,
633+ toggleCopyDecorations ,
598634 showAgentNotes ,
599635 showHelp ,
600636 showHunkHeaders ,
@@ -720,6 +756,11 @@ export function App({
720756 const diffHeaderStatsWidth = Math . min ( 24 , Math . max ( 16 , Math . floor ( diffContentWidth / 3 ) ) ) ;
721757 const diffHeaderLabelWidth = Math . max ( 8 , diffContentWidth - diffHeaderStatsWidth - 1 ) ;
722758 const diffSeparatorWidth = Math . max ( 4 , diffContentWidth - 2 ) ;
759+ // Mirror the App layout: bodyPadding/2 left-padding, then sidebar + divider when visible. Keep
760+ // this in lockstep with the body container's paddingLeft and the sidebar render branch below.
761+ const diffPaneScreenLeft =
762+ bodyPadding / 2 + ( renderSidebar ? clampedSidebarWidth + DIVIDER_WIDTH : 0 ) ;
763+ const diffPaneScreenTop = pagerMode ? 0 : 1 ;
723764
724765 return (
725766 < box
@@ -758,10 +799,14 @@ export function App({
758799 position : "relative" ,
759800 } }
760801 onMouseDrag = { updateSidebarResize }
761- onMouseDragEnd = { endSidebarResize }
802+ onMouseDragEnd = { ( event ) => {
803+ endSidebarResize ( event ) ;
804+ cancelCopySelectionRef . current ?.( ) ;
805+ } }
762806 onMouseUp = { ( event ) => {
763807 endSidebarResize ( event ) ;
764808 closeMenu ( ) ;
809+ cancelCopySelectionRef . current ?.( ) ;
765810 } }
766811 >
767812 { renderSidebar ? (
@@ -793,10 +838,14 @@ export function App({
793838 ) : null }
794839
795840 < DiffPane
841+ cancelCopySelectionRef = { cancelCopySelectionRef }
796842 codeHorizontalOffset = { codeHorizontalOffset }
843+ copyDecorations = { copyDecorations }
797844 diffContentWidth = { diffContentWidth }
798845 files = { filteredFiles }
799846 pagerMode = { pagerMode }
847+ screenLeft = { diffPaneScreenLeft }
848+ screenTop = { diffPaneScreenTop }
800849 headerLabelWidth = { diffHeaderLabelWidth }
801850 headerStatsWidth = { diffHeaderStatsWidth }
802851 layout = { resolvedLayout }
@@ -829,6 +878,7 @@ export function App({
829878 onScrollCodeHorizontally = { ( delta ) => {
830879 scrollCodeHorizontally ( delta * FAST_CODE_HORIZONTAL_SCROLL_COLUMNS ) ;
831880 } }
881+ onCopyFeedback = { showTransientNotice }
832882 onSelectFile = { jumpToFile }
833883 onViewportCenteredHunkChange = { ( fileId , hunkIndex ) =>
834884 review . selectHunk ( fileId , hunkIndex , { preserveViewport : true } )
@@ -839,12 +889,11 @@ export function App({
839889 { ! pagerMode &&
840890 ( focusArea === "filter" ||
841891 Boolean ( review . filter ) ||
842- Boolean ( sessionNoticeText ) ||
843- Boolean ( noticeText ) ) ? (
892+ Boolean ( sessionNoticeText ?? transientNoticeText ?? noticeText ) ) ? (
844893 < StatusBar
845894 filter = { review . filter }
846895 filterFocused = { focusArea === "filter" }
847- noticeText = { sessionNoticeText ?? noticeText ?? undefined }
896+ noticeText = { sessionNoticeText ?? transientNoticeText ?? noticeText ?? undefined }
848897 terminalWidth = { terminal . width }
849898 theme = { activeTheme }
850899 onCloseMenu = { closeMenu }
0 commit comments