@@ -6,20 +6,11 @@ import { ThreadId, type TurnId } from "@okcode/contracts";
66import {
77 CheckIcon ,
88 ChevronDownIcon ,
9- ChevronLeftIcon ,
10- ChevronRightIcon ,
119 Columns2Icon ,
1210 Rows3Icon ,
1311 TextWrapIcon ,
1412} from "lucide-react" ;
15- import {
16- type WheelEvent as ReactWheelEvent ,
17- useCallback ,
18- useEffect ,
19- useMemo ,
20- useRef ,
21- useState ,
22- } from "react" ;
13+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2314import { openInPreferredEditor } from "../editorPreferences" ;
2415import { gitBranchesQueryOptions } from "~/lib/gitReactQuery" ;
2516import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery" ;
@@ -44,6 +35,7 @@ import { formatShortTimestamp } from "../timestampFormat";
4435import { DiffPanelLoadingState , DiffPanelShell , type DiffPanelMode } from "./DiffPanelShell" ;
4536import { DiffStatLabel , hasNonZeroStat } from "./chat/DiffStatLabel" ;
4637import { Button } from "./ui/button" ;
38+ import { Select , SelectButton , SelectItem , SelectPopup } from "./ui/select" ;
4739import { ToggleGroup , Toggle } from "./ui/toggle-group" ;
4840
4941type DiffRenderMode = "stacked" | "split" ;
@@ -288,10 +280,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
288280 const [ diffRenderMode , setDiffRenderMode ] = useState < DiffRenderMode > ( "stacked" ) ;
289281 const [ diffWordWrap , setDiffWordWrap ] = useState ( settings . diffWordWrap ) ;
290282 const patchViewportRef = useRef < HTMLDivElement > ( null ) ;
291- const turnStripRef = useRef < HTMLDivElement > ( null ) ;
292283 const previousDiffOpenRef = useRef ( false ) ;
293- const [ canScrollTurnStripLeft , setCanScrollTurnStripLeft ] = useState ( false ) ;
294- const [ canScrollTurnStripRight , setCanScrollTurnStripRight ] = useState ( false ) ;
295284 const [ reviewStateBySelectionKey , setReviewStateBySelectionKey ] = useState <
296285 Record < string , DiffFileReviewStateByPath >
297286 > ( { } ) ;
@@ -553,153 +542,39 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
553542 } ,
554543 } ) ;
555544 } ;
556- const updateTurnStripScrollState = useCallback ( ( ) => {
557- const element = turnStripRef . current ;
558- if ( ! element ) {
559- setCanScrollTurnStripLeft ( false ) ;
560- setCanScrollTurnStripRight ( false ) ;
561- return ;
562- }
563-
564- const maxScrollLeft = Math . max ( 0 , element . scrollWidth - element . clientWidth ) ;
565- setCanScrollTurnStripLeft ( element . scrollLeft > 4 ) ;
566- setCanScrollTurnStripRight ( element . scrollLeft < maxScrollLeft - 4 ) ;
567- } , [ ] ) ;
568- const scrollTurnStripBy = useCallback ( ( offset : number ) => {
569- const element = turnStripRef . current ;
570- if ( ! element ) return ;
571- element . scrollBy ( { left : offset , behavior : "smooth" } ) ;
572- } , [ ] ) ;
573- const onTurnStripWheel = useCallback ( ( event : ReactWheelEvent < HTMLDivElement > ) => {
574- const element = turnStripRef . current ;
575- if ( ! element ) return ;
576- if ( element . scrollWidth <= element . clientWidth + 1 ) return ;
577- if ( Math . abs ( event . deltaY ) <= Math . abs ( event . deltaX ) ) return ;
578-
579- event . preventDefault ( ) ;
580- element . scrollBy ( { left : event . deltaY , behavior : "auto" } ) ;
581- } , [ ] ) ;
582-
583- useEffect ( ( ) => {
584- const element = turnStripRef . current ;
585- if ( ! element ) return ;
586-
587- const frameId = window . requestAnimationFrame ( ( ) => updateTurnStripScrollState ( ) ) ;
588- const onScroll = ( ) => updateTurnStripScrollState ( ) ;
589-
590- element . addEventListener ( "scroll" , onScroll , { passive : true } ) ;
591-
592- const resizeObserver = new ResizeObserver ( ( ) => updateTurnStripScrollState ( ) ) ;
593- resizeObserver . observe ( element ) ;
594-
595- return ( ) => {
596- window . cancelAnimationFrame ( frameId ) ;
597- element . removeEventListener ( "scroll" , onScroll ) ;
598- resizeObserver . disconnect ( ) ;
599- } ;
600- } , [ updateTurnStripScrollState ] ) ;
601-
602- useEffect ( ( ) => {
603- const frameId = window . requestAnimationFrame ( ( ) => updateTurnStripScrollState ( ) ) ;
604- return ( ) => {
605- window . cancelAnimationFrame ( frameId ) ;
606- } ;
607- } , [ orderedTurnDiffSummaries , selectedTurnId , updateTurnStripScrollState ] ) ;
608-
609- useEffect ( ( ) => {
610- const element = turnStripRef . current ;
611- if ( ! element ) return ;
612-
613- const selectedChip = element . querySelector < HTMLElement > ( "[data-turn-chip-selected='true']" ) ;
614- selectedChip ?. scrollIntoView ( { block : "nearest" , inline : "nearest" , behavior : "smooth" } ) ;
615- } , [ selectedTurn ?. turnId , selectedTurnId ] ) ;
545+ const turnSelectValue = selectedTurnId ?? "all" ;
546+ const handleTurnSelectChange = useCallback (
547+ ( value : string | null ) => {
548+ if ( value === "all" || value === null ) {
549+ selectWholeConversation ( ) ;
550+ } else {
551+ selectTurn ( value as TurnId ) ;
552+ }
553+ } ,
554+ [ selectTurn , selectWholeConversation ] ,
555+ ) ;
616556
617557 const headerRow = (
618558 < >
619- < div className = "relative min-w-0 flex-1 [-webkit-app-region:no-drag]" >
620- { canScrollTurnStripLeft && (
621- < div className = "pointer-events-none absolute inset-y-0 left-8 z-10 w-7 bg-linear-to-r from-card to-transparent" />
622- ) }
623- { canScrollTurnStripRight && (
624- < div className = "pointer-events-none absolute inset-y-0 right-8 z-10 w-7 bg-linear-to-l from-card to-transparent" />
625- ) }
626- < button
627- type = "button"
628- className = { cn (
629- "absolute left-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors" ,
630- canScrollTurnStripLeft
631- ? "border-border/70 hover:border-border hover:text-foreground"
632- : "cursor-not-allowed border-border/40 text-muted-foreground/40" ,
633- ) }
634- onClick = { ( ) => scrollTurnStripBy ( - 180 ) }
635- disabled = { ! canScrollTurnStripLeft }
636- aria-label = "Scroll change list left"
637- >
638- < ChevronLeftIcon className = "size-3.5" />
639- </ button >
640- < button
641- type = "button"
642- className = { cn (
643- "absolute right-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors" ,
644- canScrollTurnStripRight
645- ? "border-border/70 hover:border-border hover:text-foreground"
646- : "cursor-not-allowed border-border/40 text-muted-foreground/40" ,
647- ) }
648- onClick = { ( ) => scrollTurnStripBy ( 180 ) }
649- disabled = { ! canScrollTurnStripRight }
650- aria-label = "Scroll change list right"
651- >
652- < ChevronRightIcon className = "size-3.5" />
653- </ button >
654- < div
655- ref = { turnStripRef }
656- className = "turn-chip-strip flex gap-1 overflow-x-auto px-8 py-0.5"
657- onWheel = { onTurnStripWheel }
658- >
659- < button
660- type = "button"
661- className = "shrink-0 rounded-md"
662- onClick = { selectWholeConversation }
663- data-turn-chip-selected = { selectedTurnId === null }
664- >
665- < div
666- className = { cn (
667- "rounded-md border px-2 py-1 text-left transition-colors" ,
668- selectedTurnId === null
669- ? "border-border bg-accent text-accent-foreground"
670- : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80" ,
671- ) }
672- >
673- < div className = "text-[10px] leading-tight font-medium" > All changes</ div >
674- </ div >
675- </ button >
676- { orderedTurnDiffSummaries . map ( ( summary ) => (
677- < button
678- key = { summary . turnId }
679- type = "button"
680- className = "shrink-0 rounded-md"
681- onClick = { ( ) => selectTurn ( summary . turnId ) }
682- title = { `${
683- summary . turnId === latestSelectedTurnId
684- ? "Latest change"
685- : `Change ${
686- summary . checkpointTurnCount ??
687- inferredCheckpointTurnCountByTurnId [ summary . turnId ] ??
688- "?"
689- } `
690- } • ${ formatShortTimestamp ( summary . completedAt , settings . timestampFormat ) } `}
691- data-turn-chip-selected = { summary . turnId === selectedTurn ?. turnId }
692- >
693- < div
694- className = { cn (
695- "rounded-md border px-2 py-1 text-left transition-colors" ,
696- summary . turnId === selectedTurn ?. turnId
697- ? "border-border bg-accent text-accent-foreground"
698- : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80" ,
699- ) }
700- >
701- < div className = "flex flex-col gap-0.5" >
702- < span className = "text-[10px] leading-tight font-medium" >
559+ < div className = "min-w-0 flex-1 [-webkit-app-region:no-drag]" >
560+ < Select value = { turnSelectValue } onValueChange = { handleTurnSelectChange } >
561+ < SelectButton size = "xs" variant = "ghost" >
562+ { selectedTurnId === null
563+ ? "All changes"
564+ : selectedTurn ?. turnId === latestSelectedTurnId
565+ ? `Latest • ${ formatShortTimestamp ( selectedTurn . completedAt , settings . timestampFormat ) } `
566+ : `Change ${
567+ selectedTurn ?. checkpointTurnCount ??
568+ ( selectedTurn ? inferredCheckpointTurnCountByTurnId [ selectedTurn . turnId ] : null ) ??
569+ "?"
570+ } • ${ selectedTurn ? formatShortTimestamp ( selectedTurn . completedAt , settings . timestampFormat ) : "" } `}
571+ </ SelectButton >
572+ < SelectPopup >
573+ < SelectItem value = "all" > All changes</ SelectItem >
574+ { orderedTurnDiffSummaries . map ( ( summary ) => (
575+ < SelectItem key = { summary . turnId } value = { summary . turnId } >
576+ < span className = "flex items-center justify-between gap-3" >
577+ < span >
703578 { summary . turnId === latestSelectedTurnId
704579 ? "Latest"
705580 : `Change ${
@@ -708,14 +583,14 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
708583 "?"
709584 } `}
710585 </ span >
711- < span className = "text-[9px] leading-tight opacity-60 " >
586+ < span className = "text-muted-foreground text-xs " >
712587 { formatShortTimestamp ( summary . completedAt , settings . timestampFormat ) }
713588 </ span >
714- </ div >
715- </ div >
716- </ button >
717- ) ) }
718- </ div >
589+ </ span >
590+ </ SelectItem >
591+ ) ) }
592+ </ SelectPopup >
593+ </ Select >
719594 </ div >
720595 < div className = "flex shrink-0 items-center gap-1 [-webkit-app-region:no-drag]" >
721596 < ToggleGroup
0 commit comments