diff --git a/packages/review-editor/components/AllFilesDiffView.tsx b/packages/review-editor/components/AllFilesDiffView.tsx index aabac289..3effd001 100644 --- a/packages/review-editor/components/AllFilesDiffView.tsx +++ b/packages/review-editor/components/AllFilesDiffView.tsx @@ -101,6 +101,7 @@ export const AllFilesDiffView: React.FC = ({ setActiveFilePath(null); setCollapsedFiles(new Set()); collapseHistory.current = []; + setFileCommentAnchor(null); }, [files]); const scrollRef = useRef(null); @@ -515,9 +516,11 @@ export const AllFilesDiffView: React.FC = ({ {fileCommentAnchor && onAddFileComment && ( { onAddFileComment(fileCommentAnchor.filePath, text); setFileCommentAnchor(null); diff --git a/packages/review-editor/components/DiffViewer.tsx b/packages/review-editor/components/DiffViewer.tsx index 912737c1..fd99a2b4 100644 --- a/packages/review-editor/components/DiffViewer.tsx +++ b/packages/review-editor/components/DiffViewer.tsx @@ -120,6 +120,9 @@ interface DiffViewerProps { oldPath?: string; /** Base branch override used for file-content lookups (branch / merge-base modes only). */ reviewBase?: string; + /** Current PR url + diff scope — used to namespace file-comment drafts so they don't leak across in-place PR switches. */ + prUrl?: string; + prDiffScope?: string; isFocused?: boolean; diffStyle: 'split' | 'unified'; diffOverflow?: 'scroll' | 'wrap'; @@ -165,6 +168,8 @@ export const DiffViewer: React.FC = ({ filePath, oldPath, reviewBase, + prUrl, + prDiffScope, isFocused = false, diffStyle, diffOverflow, @@ -621,6 +626,7 @@ export const DiffViewer: React.FC = ({ anchorEl={fileCommentAnchor} contextText={filePath.split('/').pop() || filePath} isGlobal={false} + draftKey={`file:${prUrl ?? ''}:${prDiffScope ?? ''}:${filePath}`} onSubmit={(text) => { onAddFileComment(text); setFileCommentAnchor(null); diff --git a/packages/review-editor/dock/panels/ReviewDiffPanel.tsx b/packages/review-editor/dock/panels/ReviewDiffPanel.tsx index 7de62e3e..5e08df4f 100644 --- a/packages/review-editor/dock/panels/ReviewDiffPanel.tsx +++ b/packages/review-editor/dock/panels/ReviewDiffPanel.tsx @@ -71,6 +71,8 @@ export const ReviewDiffPanel: React.FC = (props) => { filePath={file.path} oldPath={file.oldPath} reviewBase={state.reviewBase} + prUrl={state.prMetadata?.url} + prDiffScope={state.prDiffScope} isFocused={isFocusedFile} diffStyle={state.diffStyle} diffOverflow={state.diffOverflow} diff --git a/packages/ui/components/CommentPopover.tsx b/packages/ui/components/CommentPopover.tsx index 4baa1175..d56e50f6 100644 --- a/packages/ui/components/CommentPopover.tsx +++ b/packages/ui/components/CommentPopover.tsx @@ -20,11 +20,28 @@ interface CommentPopoverProps { onSubmit: (text: string, images?: ImageAttachment[]) => void; /** Called when popover is closed/cancelled */ onClose: () => void; + /** Opt-in: persist text + images across close/reopen, keyed by this string. Cleared on submit. */ + draftKey?: string; } const MAX_POPOVER_WIDTH = 384; const GAP = 8; +// Module-level draft store: survives popover unmount so reopening the same key restores in-progress text. +const draftStore = new Map(); + +/** Mirrors the latest text + images into `draftStore[draftKey]` so they outlive popover unmount. No-op without a key. */ +function useCommentDraftSync(draftKey: string | undefined, text: string, images: ImageAttachment[]) { + useEffect(() => { + if (!draftKey) return; + if (text.trim() || images.length > 0) { + draftStore.set(draftKey, { text, images }); + } else { + draftStore.delete(draftKey); + } + }, [draftKey, text, images]); +} + function computePosition(anchorRect: DOMRect): { top: number; left: number; flipAbove: boolean; width: number } { const spaceBelow = window.innerHeight - anchorRect.bottom; const flipAbove = spaceBelow < 280; @@ -48,15 +65,19 @@ export const CommentPopover: React.FC = ({ initialText = '', onSubmit, onClose, + draftKey, }) => { const [mode, setMode] = useState<'popover' | 'dialog'>('popover'); - const [text, setText] = useState(initialText); - const [images, setImages] = useState([]); + const initialDraft = draftKey ? draftStore.get(draftKey) : undefined; + const [text, setText] = useState(initialDraft?.text ?? initialText); + const [images, setImages] = useState(initialDraft?.images ?? []); const [position, setPosition] = useState<{ top: number; left: number; flipAbove: boolean; width: number } | null>(null); const textareaRef = useRef(null); const popoverRef = useRef(null); const { dragPosition, dragHandleProps, wasDragged, reset: resetDrag } = useDraggable(popoverRef); + useCommentDraftSync(draftKey, text, images); + // Reset drag when anchor changes (new annotation) or mode switches useEffect(() => { resetDrag(); }, [anchorEl, anchorRect, resetDrag]); useEffect(() => { if (mode === 'popover') resetDrag(); }, [mode, resetDrag]); @@ -111,9 +132,10 @@ export const CommentPopover: React.FC = ({ const handleSubmit = useCallback(() => { if (text.trim() || images.length > 0) { + if (draftKey) draftStore.delete(draftKey); onSubmit(text, images.length > 0 ? images : undefined); } - }, [text, images, onSubmit]); + }, [text, images, onSubmit, draftKey]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') {