@@ -40,6 +40,7 @@ import {
4040 getRenderablePatch ,
4141 resolveDiffCopyText ,
4242 resolveDiffThemeName ,
43+ serializeRenderablePatchText ,
4344 summarizePatchStats ,
4445} from "../lib/diffRendering" ;
4546import { resolveDiffEnvironmentState } from "../lib/threadEnvironment" ;
@@ -56,7 +57,7 @@ import { getProviderStartOptions, useAppSettings } from "../appSettings";
5657import { useComposerDraftStore } from "../composerDraftStore" ;
5758import { formatShortTimestamp } from "../timestampFormat" ;
5859import ChatMarkdown from "./ChatMarkdown" ;
59- import { resolveDiffPanelThread } from "./DiffPanel.logic" ;
60+ import { resolveDiffPanelThread , resolveDiffSelectAllArmed } from "./DiffPanel.logic" ;
6061import { DiffPanelLoadingState , DiffPanelShell , type DiffPanelMode } from "./DiffPanelShell" ;
6162import { Button } from "./ui/button" ;
6263import { Menu , MenuPopup , MenuRadioGroup , MenuRadioItem , MenuTrigger } from "./ui/menu" ;
@@ -195,6 +196,8 @@ export default function DiffPanel({
195196 const setRepoDiffScope = useRepoDiffScopeStore ( ( store ) => store . setScope ) ;
196197 const [ collapsedFiles , setCollapsedFiles ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
197198 const patchViewportRef = useRef < HTMLDivElement > ( null ) ;
199+ // Tracks an in-flight "select all then copy" gesture inside the virtualized diff surface.
200+ const diffSelectAllArmedRef = useRef ( false ) ;
198201 const turnStripRef = useRef < HTMLDivElement > ( null ) ;
199202 const previousDiffOpenRef = useRef ( false ) ;
200203 const [ canScrollTurnStripLeft , setCanScrollTurnStripLeft ] = useState ( false ) ;
@@ -403,11 +406,16 @@ export default function DiffPanel({
403406 const isSidebarMode = mode === "sidebar" ;
404407 const { copyToClipboard, isCopied : isSummaryCopied } = useCopyToClipboard ( ) ;
405408 const { copyToClipboard : copyDiffToClipboard , isCopied : isDiffCopied } = useCopyToClipboard ( ) ;
406- const diffCopyText = useMemo ( ( ) => resolveDiffCopyText ( activeReviewPatch ) , [ activeReviewPatch ] ) ;
407409 const renderablePatch = useMemo (
408410 ( ) => getRenderablePatch ( activeReviewPatch , `diff-panel:${ resolvedTheme } ` ) ,
409411 [ activeReviewPatch , resolvedTheme ] ,
410412 ) ;
413+ // Serialize the full diff straight from the parsed model so copy paths never depend on
414+ // which virtualized rows happen to be mounted in the DOM.
415+ const diffCopyText = useMemo (
416+ ( ) => serializeRenderablePatchText ( renderablePatch ) ?? resolveDiffCopyText ( activeReviewPatch ) ,
417+ [ renderablePatch , activeReviewPatch ] ,
418+ ) ;
411419 const renderableFiles = useMemo ( ( ) => {
412420 if ( ! renderablePatch || renderablePatch . kind !== "files" ) {
413421 return [ ] ;
@@ -507,16 +515,10 @@ export default function DiffPanel({
507515 ? "Failed to generate diff summary."
508516 : null ;
509517 const canShowSummary = Boolean (
510- ! diffEnvironmentPending &&
511- activeCwd &&
512- ( ! hasResolvedRepoPatch || ! hasNoRepoChanges ) ,
518+ ! diffEnvironmentPending && activeCwd && ( ! hasResolvedRepoPatch || ! hasNoRepoChanges ) ,
513519 ) ;
514520 const canPrefetchSummary = Boolean (
515- diffOpen &&
516- ! diffEnvironmentPending &&
517- activeCwd &&
518- normalizedRepoPatch &&
519- ! hasNoRepoChanges ,
521+ diffOpen && ! diffEnvironmentPending && activeCwd && normalizedRepoPatch && ! hasNoRepoChanges ,
520522 ) ;
521523 const canShowTotal = Boolean ( ! diffEnvironmentPending && activeCwd ) ;
522524
@@ -565,6 +567,49 @@ export default function DiffPanel({
565567 } ) ;
566568 } , [ ] ) ;
567569
570+ // The diff surface is virtualized and renders into shadow DOM, so a native
571+ // "select all + copy" only captures the handful of mounted rows. We watch the
572+ // document: a Cmd/Ctrl+A keydown still passes through the viewport element (so we can
573+ // tell the gesture started in the diff), and the matching `copy` event — which does
574+ // *not* travel through the viewport — is then hijacked to write the fully serialized
575+ // diff so every line reaches the clipboard.
576+ useEffect ( ( ) => {
577+ const handleKeyDown = ( event : KeyboardEvent ) => {
578+ const viewport = patchViewportRef . current ;
579+ const isWithinDiffViewport = viewport ? event . composedPath ( ) . includes ( viewport ) : false ;
580+ diffSelectAllArmedRef . current = resolveDiffSelectAllArmed (
581+ diffSelectAllArmedRef . current ,
582+ event ,
583+ isWithinDiffViewport ,
584+ ) ;
585+ } ;
586+ const handlePointerDown = ( ) => {
587+ // Any fresh pointer interaction ends the select-all gesture.
588+ diffSelectAllArmedRef . current = false ;
589+ } ;
590+ const handleCopy = ( event : ClipboardEvent ) => {
591+ if ( ! diffSelectAllArmedRef . current ) {
592+ return ;
593+ }
594+ // One-shot: the next deliberate select-all must re-arm it.
595+ diffSelectAllArmedRef . current = false ;
596+ if ( ! diffCopyText || ! event . clipboardData ) {
597+ return ;
598+ }
599+ event . preventDefault ( ) ;
600+ event . clipboardData . setData ( "text/plain" , diffCopyText ) ;
601+ } ;
602+
603+ document . addEventListener ( "keydown" , handleKeyDown , true ) ;
604+ document . addEventListener ( "pointerdown" , handlePointerDown , true ) ;
605+ document . addEventListener ( "copy" , handleCopy , true ) ;
606+ return ( ) => {
607+ document . removeEventListener ( "keydown" , handleKeyDown , true ) ;
608+ document . removeEventListener ( "pointerdown" , handlePointerDown , true ) ;
609+ document . removeEventListener ( "copy" , handleCopy , true ) ;
610+ } ;
611+ } , [ diffCopyText ] ) ;
612+
568613 const selectTurn = ( turnId : TurnId ) => {
569614 if ( ! activeThread ) return ;
570615 if ( onUpdatePanelState ) {
@@ -943,8 +988,8 @@ export default function DiffPanel({
943988 < div className = "min-w-0" >
944989 < p className = "text-sm font-medium text-foreground" > Repo summary</ p >
945990 < p className = "text-[11px] text-muted-foreground" >
946- Generated from the current{ " " }
947- { REPO_DIFF_SCOPE_LABELS [ repoDiffScope ] . toLowerCase ( ) } diff.
991+ Generated from the current { REPO_DIFF_SCOPE_LABELS [ repoDiffScope ] . toLowerCase ( ) } { " " }
992+ diff.
948993 </ p >
949994 </ div >
950995 { diffSummaryText ? (
0 commit comments