({ messages, onDone }: Props)
| 53 | } |
| 54 | |
| 55 | export function DiffDialog({ messages, onDone }: Props): React.ReactNode { |
| 56 | const gitDiffData = useDiffData(); |
| 57 | const turnDiffs = useTurnDiffs(messages); |
| 58 | |
| 59 | const [viewMode, setViewMode] = useState<ViewMode>('list'); |
| 60 | const [selectedIndex, setSelectedIndex] = useState<number>(0); |
| 61 | const [sourceIndex, setSourceIndex] = useState<number>(0); |
| 62 | |
| 63 | const sources: DiffSource[] = useMemo( |
| 64 | () => [{ type: 'current' }, ...turnDiffs.map((turn): DiffSource => ({ type: 'turn', turn }))], |
| 65 | [turnDiffs], |
| 66 | ); |
| 67 | |
| 68 | const currentSource = sources[sourceIndex]; |
| 69 | const currentTurn = currentSource?.type === 'turn' ? currentSource.turn : null; |
| 70 | |
| 71 | const diffData = useMemo((): DiffData => { |
| 72 | return currentTurn ? turnDiffToDiffData(currentTurn) : gitDiffData; |
| 73 | }, [currentTurn, gitDiffData]); |
| 74 | |
| 75 | const selectedFile = diffData.files[selectedIndex]; |
| 76 | const selectedHunks = useMemo(() => { |
| 77 | return selectedFile ? diffData.hunks.get(selectedFile.path) || [] : []; |
| 78 | }, [selectedFile, diffData.hunks]); |
| 79 | |
| 80 | // Clamp sourceIndex when sources shrink (e.g., conversation rewind) |
| 81 | useEffect(() => { |
| 82 | if (sourceIndex >= sources.length) { |
| 83 | setSourceIndex(Math.max(0, sources.length - 1)); |
| 84 | } |
| 85 | }, [sources.length, sourceIndex]); |
| 86 | |
| 87 | // Reset file selection when source changes |
| 88 | const prevSourceIndex = useRef(sourceIndex); |
| 89 | useEffect(() => { |
| 90 | if (prevSourceIndex.current !== sourceIndex) { |
| 91 | setSelectedIndex(0); |
| 92 | prevSourceIndex.current = sourceIndex; |
| 93 | } |
| 94 | }, [sourceIndex]); |
| 95 | |
| 96 | // Register as modal overlay so Chat keybindings and CancelRequestHandler |
| 97 | // are disabled while DiffDialog is showing |
| 98 | useRegisterOverlay('diff-dialog'); |
| 99 | |
| 100 | // Diff dialog navigation keybindings |
| 101 | // View-mode dependent: left/right arrows have different behavior based on mode |
| 102 | // (source tab switching vs back navigation), and up/down/enter are |
| 103 | // context-sensitive to viewMode |
| 104 | // |
| 105 | // Note: Escape handling (diff:dismiss) is NOT registered here because Dialog's |
| 106 | // built-in useKeybinding('confirm:no', handleCancel) already handles it. |
| 107 | // Having both would be dead code since Dialog's child effect registers first |
| 108 | // and calls stopImmediatePropagation(). The diff:dismiss binding in |
| 109 | // defaultBindings.ts is kept for useShortcutDisplay to show the "esc close" hint. |
| 110 | useKeybindings( |
| 111 | { |
| 112 | // Left arrow: in detail mode goes back, in list mode switches source |
nothing calls this directly
no test coverage detected