diff --git a/ui/src/components/DiffViewer.tsx b/ui/src/components/DiffViewer.tsx index a2bb6f1ec177fc2e75dfd085e5159767fd0a7527..1c5daca70110185b73878c54ab9b7b95d4f5a533 100644 --- a/ui/src/components/DiffViewer.tsx +++ b/ui/src/components/DiffViewer.tsx @@ -10,6 +10,33 @@ interface DiffViewerProps { onCommentTextChange: (text: string) => void; } +// Icon components for cleaner JSX +const PrevFileIcon = () => ( + + + + +); + +const PrevChangeIcon = () => ( + + + +); + +const NextChangeIcon = () => ( + + + +); + +const NextFileIcon = () => ( + + + + +); + // Global Monaco instance - loaded lazily let monacoInstance: typeof Monaco | null = null; let monacoLoadPromise: Promise | null = null; @@ -75,11 +102,12 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro } | null>(null); const [commentText, setCommentText] = useState(""); const [mode, setMode] = useState("comment"); - const [selectorsExpanded, setSelectorsExpanded] = useState(false); + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const editorContainerRef = useRef(null); const editorRef = useRef(null); const monacoRef = useRef(null); + const commentInputRef = useRef(null); const modeRef = useRef(mode); // Keep modeRef in sync with mode state and update editor options @@ -101,6 +129,16 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro return () => window.removeEventListener("resize", handleResize); }, []); + // Focus comment input when dialog opens + useEffect(() => { + if (showCommentDialog && commentInputRef.current) { + // Small delay to ensure the dialog is rendered + setTimeout(() => { + commentInputRef.current?.focus(); + }, 50); + } + }, [showCommentDialog]); + // Load Monaco when viewer opens useEffect(() => { if (isOpen && !monacoLoaded) { @@ -116,10 +154,25 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro } }, [isOpen, monacoLoaded]); - // Load diffs when viewer opens + // Load diffs when viewer opens, reset state when it closes useEffect(() => { if (isOpen && cwd) { loadDiffs(); + } else if (!isOpen) { + // Reset state when closing + setFileDiff(null); + setSelectedFile(null); + setFiles([]); + setSelectedDiff(null); + setDiffs([]); + setError(null); + setShowCommentDialog(null); + setCommentText(""); + // Dispose editor when closing + if (editorRef.current) { + editorRef.current.dispose(); + editorRef.current = null; + } } }, [isOpen, cwd]); @@ -183,7 +236,7 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro renderMarginRevertIcon: false, lineNumbers: isMobile ? "off" : "on", minimap: { enabled: false }, - scrollBeyondLastLine: false, + scrollBeyondLastLine: true, // Enable scroll past end for mobile floating buttons wordWrap: "on", glyphMargin: false, // No glyph margin - click on lines to comment lineDecorationsWidth: isMobile ? 0 : 10, @@ -195,6 +248,7 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro contextmenu: false, links: false, folding: !isMobile, + padding: isMobile ? { bottom: 80 } : undefined, // Extra padding for floating buttons on mobile }); diffEditor.setModel({ @@ -206,6 +260,32 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro // Add click handler for commenting - clicking on a line in comment mode opens dialog const modifiedEditor = diffEditor.getModifiedEditor(); + + // Handler function for opening comment dialog + const openCommentDialog = (lineNumber: number) => { + const model = modifiedEditor.getModel(); + const selection = modifiedEditor.getSelection(); + let selectedText = ""; + let startLine = lineNumber; + let endLine = lineNumber; + + if (selection && !selection.isEmpty() && model) { + selectedText = model.getValueInRange(selection); + startLine = selection.startLineNumber; + endLine = selection.endLineNumber; + } else if (model) { + selectedText = model.getLineContent(lineNumber) || ""; + } + + setShowCommentDialog({ + line: startLine, + side: "right", + selectedText, + startLine, + endLine, + }); + }; + modifiedEditor.onMouseDown((e: Monaco.editor.IEditorMouseEvent) => { // In comment mode, clicking on line content opens comment dialog const isLineClick = @@ -215,30 +295,28 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro if (isLineClick && modeRef.current === "comment") { const position = e.target.position; if (position) { - const model = modifiedEditor.getModel(); - const selection = modifiedEditor.getSelection(); - let selectedText = ""; - let startLine = position.lineNumber; - let endLine = position.lineNumber; - - if (selection && !selection.isEmpty() && model) { - selectedText = model.getValueInRange(selection); - startLine = selection.startLineNumber; - endLine = selection.endLineNumber; - } else if (model) { - selectedText = model.getLineContent(position.lineNumber) || ""; - } - - setShowCommentDialog({ - line: startLine, - side: "right", - selectedText, - startLine, - endLine, - }); + openCommentDialog(position.lineNumber); } } }); + + // For mobile: use onMouseUp which fires more reliably on touch devices + if (isMobile) { + modifiedEditor.onMouseUp((e: Monaco.editor.IEditorMouseEvent) => { + if (modeRef.current !== "comment") return; + + const isLineClick = + e.target.type === monaco.editor.MouseTargetType.CONTENT_TEXT || + e.target.type === monaco.editor.MouseTargetType.CONTENT_EMPTY; + + if (isLineClick) { + const position = e.target.position; + if (position) { + openCommentDialog(position.lineNumber); + } + } + }); + } // Add content change listener for auto-save contentChangeDisposableRef.current?.dispose(); @@ -320,18 +398,13 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro const handleAddComment = () => { if (!showCommentDialog || !commentText.trim() || !selectedFile) return; - // Format and append comment directly to the message input - let commentBlock = `**${selectedFile}**`; - if (showCommentDialog.startLine !== showCommentDialog.endLine) { - commentBlock += ` (lines ${showCommentDialog.startLine}-${showCommentDialog.endLine})`; - } else { - commentBlock += ` (line ${showCommentDialog.line})`; - } - commentBlock += ":\n"; - if (showCommentDialog.selectedText) { - commentBlock += "```\n" + showCommentDialog.selectedText + "\n```\n"; - } - commentBlock += commentText + "\n\n"; + // Format: > filename:123: code + // Comment... + const line = showCommentDialog.line; + const codeSnippet = showCommentDialog.selectedText?.split('\n')[0]?.trim() || ''; + const truncatedCode = codeSnippet.length > 60 ? codeSnippet.substring(0, 57) + '...' : codeSnippet; + + const commentBlock = `> ${selectedFile}:${line}: ${truncatedCode}\n${commentText}\n\n`; onCommentTextChange(commentBlock); setShowCommentDialog(null); @@ -542,6 +615,101 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro const hasNextFile = currentFileIndex < files.length - 1; const hasPrevFile = currentFileIndex > 0; + // Selectors shared between desktop and mobile + const commitSelector = ( + + ); + + const fileSelector = ( + + ); + + const modeToggle = ( +
+ + +
+ ); + + const navButtons = ( +
+ + + + +
+ ); + return (
@@ -554,129 +722,36 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro
)} - {/* Header */} -
-
- {/* Mode toggle */} -
- - -
- - {/* Navigation buttons: <<(prev file) <(prev change) >(next change) >>(next file) */} -
- - - - + {/* Header - different layout for desktop vs mobile */} + {isMobile ? ( + // Mobile header: just selectors 50/50 +
+
+ {commitSelector} + {fileSelector}
- - {/* Expand/collapse selectors */} - -
- - {/* Collapsible selectors */} - {selectorsExpanded && ( -
- {/* Diff selector */} - - - {/* File selector */} - + ) : ( + // Desktop header: selectors expand, controls on right +
+
+
+ {commitSelector} + {fileSelector} +
+
+ {navButtons} + {modeToggle} + +
- )} -
+
+ )} {/* Error banner */} {error &&
{error}
} @@ -712,6 +787,51 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro />
+ {/* Mobile floating nav buttons at bottom */} + {isMobile && ( +
+ + + + + +
+ )} + {/* Comment dialog */} {showCommentDialog && (
@@ -726,6 +846,7 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange }: DiffViewerPro
{showCommentDialog.selectedText}
)}