shelley: Add hover highlighting and comment indicator to Monaco diff view

Philip Zeyliger created

Prompt: in a new worktree, make it so that when i mouse over a line in the monaco diff view, that line is highlighted. Maybe there's a subtle indicator that you can add a comment if you click on it.

- Use Monaco's decoration API to highlight the hovered line with a subtle blue tint
- Show a comment bubble icon in the glyph margin when hovering over a line
- Enable glyph margin on non-mobile devices for the comment indicator
- Clear decorations when mouse leaves the editor
- Works with both light and dark themes

Change summary

ui/src/components/PatchTool.tsx | 44 ++++++++++++++++++++++++++++++++++
ui/src/styles.css               | 32 +++++++++++++++++++++++++
2 files changed, 75 insertions(+), 1 deletion(-)

Detailed changes

ui/src/components/PatchTool.tsx 🔗

@@ -85,6 +85,7 @@ function PatchTool({
   const editorRef = useRef<Monaco.editor.IStandaloneDiffEditor | null>(null);
   const monacoRef = useRef<typeof Monaco | null>(null);
   const commentInputRef = useRef<HTMLTextAreaElement>(null);
+  const hoverDecorationsRef = useRef<string[]>([]);
 
   // Track viewport size
   useEffect(() => {
@@ -192,7 +193,7 @@ function PatchTool({
       minimap: { enabled: false },
       scrollBeyondLastLine: false,
       wordWrap: "on",
-      glyphMargin: false,
+      glyphMargin: !isMobile, // Enable glyph margin for comment indicator
       lineDecorationsWidth: isMobile ? 0 : 10,
       lineNumbersMinChars: isMobile ? 0 : 3,
       quickSuggestions: false,
@@ -244,6 +245,47 @@ function PatchTool({
           }
         }
       });
+
+      // Add hover highlighting with comment indicator
+      let lastHoveredLine = -1;
+      modifiedEditor.onMouseMove((e: Monaco.editor.IEditorMouseEvent) => {
+        const position = e.target.position;
+        const lineNumber = position?.lineNumber ?? -1;
+
+        if (lineNumber === lastHoveredLine) return;
+        lastHoveredLine = lineNumber;
+
+        if (lineNumber > 0) {
+          hoverDecorationsRef.current = modifiedEditor.deltaDecorations(
+            hoverDecorationsRef.current,
+            [
+              {
+                range: new monaco.Range(lineNumber, 1, lineNumber, 1),
+                options: {
+                  isWholeLine: true,
+                  className: "patch-line-hover",
+                  glyphMarginClassName: "patch-comment-glyph",
+                },
+              },
+            ],
+          );
+        } else {
+          // Clear decorations when not hovering a line
+          hoverDecorationsRef.current = modifiedEditor.deltaDecorations(
+            hoverDecorationsRef.current,
+            [],
+          );
+        }
+      });
+
+      // Clear decorations when mouse leaves editor
+      modifiedEditor.onMouseLeave(() => {
+        lastHoveredLine = -1;
+        hoverDecorationsRef.current = modifiedEditor.deltaDecorations(
+          hoverDecorationsRef.current,
+          [],
+        );
+      });
     }
 
     // Cleanup function

ui/src/styles.css 🔗

@@ -1238,6 +1238,38 @@ button {
   overflow: hidden;
 }
 
+/* Monaco line hover highlighting (via decoration API) */
+.patch-tool-monaco-editor .monaco-editor .patch-line-hover {
+  background-color: rgba(37, 99, 235, 0.08) !important;
+}
+
+.dark .patch-tool-monaco-editor .monaco-editor .patch-line-hover {
+  background-color: rgba(96, 165, 250, 0.12) !important;
+}
+
+/* Comment indicator glyph in margin */
+.patch-tool-monaco-editor .monaco-editor .patch-comment-glyph {
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'%3E%3C/path%3E%3Cline x1='9' y1='10' x2='15' y2='10'%3E%3C/line%3E%3C/svg%3E");
+  background-size: 14px 14px;
+  background-repeat: no-repeat;
+  background-position: center;
+  opacity: 0.6;
+  cursor: pointer;
+}
+
+.patch-tool-monaco-editor .monaco-editor .patch-comment-glyph:hover {
+  opacity: 1;
+}
+
+.dark .patch-tool-monaco-editor .monaco-editor .patch-comment-glyph {
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'%3E%3C/path%3E%3Cline x1='9' y1='10' x2='15' y2='10'%3E%3C/line%3E%3C/svg%3E");
+}
+
+/* Make Monaco lines clickable with pointer cursor */
+.patch-tool-monaco-editor .monaco-editor .view-lines {
+  cursor: pointer;
+}
+
 /* Patch Tool Comment Dialog */
 .patch-tool-comment-dialog {
   position: fixed;