shelley: apply light/dark mode setting to Monaco diff viewer

Philip Zeyliger created

Prompt: in a new worktree, reset to origin/main. The light/dark mode setting doesn't apply yet to the monaco diff view; fix that; it should apply there too.

- Add isDarkModeActive() helper to theme service
- Set Monaco theme based on current dark mode state when creating editor
- Use MutationObserver to watch for dark class changes on documentElement
- Dynamically update Monaco theme via monaco.editor.setTheme() when mode changes

The Monaco diff viewer now correctly shows 'vs-dark' theme in dark mode and
'vs' theme in light mode, and updates instantly when the user toggles themes.

Change summary

ui/src/components/DiffViewer.tsx | 26 +++++++++++++++++++++++++-
ui/src/services/theme.ts         |  5 +++++
2 files changed, 30 insertions(+), 1 deletion(-)

Detailed changes

ui/src/components/DiffViewer.tsx 🔗

@@ -1,6 +1,7 @@
 import React, { useState, useEffect, useCallback, useRef } from "react";
 import type * as Monaco from "monaco-editor";
 import { api } from "../services/api";
+import { isDarkModeActive } from "../services/theme";
 import { GitDiffInfo, GitFileInfo, GitFileDiff } from "../types";
 
 interface DiffViewerProps {
@@ -227,7 +228,7 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange, initialCommit }
 
     // Create diff editor with mobile-friendly options
     const diffEditor = monaco.editor.createDiffEditor(editorContainerRef.current, {
-      theme: "vs",
+      theme: isDarkModeActive() ? "vs-dark" : "vs",
       readOnly: true, // Always read-only in diff viewer
       originalEditable: false,
       automaticLayout: true,
@@ -578,6 +579,29 @@ function DiffViewer({ cwd, isOpen, onClose, onCommentTextChange, initialCommit }
     saveCurrentFile();
   }, [saveCurrentFile]);
 
+  // Update Monaco theme when dark mode changes
+  useEffect(() => {
+    if (!monacoRef.current) return;
+
+    const updateMonacoTheme = () => {
+      const theme = isDarkModeActive() ? "vs-dark" : "vs";
+      monacoRef.current?.editor.setTheme(theme);
+    };
+
+    // Watch for changes to the dark class on documentElement
+    const observer = new MutationObserver((mutations) => {
+      for (const mutation of mutations) {
+        if (mutation.attributeName === "class") {
+          updateMonacoTheme();
+        }
+      }
+    });
+
+    observer.observe(document.documentElement, { attributes: true });
+
+    return () => observer.disconnect();
+  }, [monacoLoaded]);
+
   // Keyboard shortcuts
   useEffect(() => {
     if (!isOpen) return;

ui/src/services/theme.ts 🔗

@@ -18,6 +18,11 @@ export function getSystemPrefersDark(): boolean {
   return window.matchMedia("(prefers-color-scheme: dark)").matches;
 }
 
+export function isDarkModeActive(): boolean {
+  const theme = getStoredTheme();
+  return theme === "dark" || (theme === "system" && getSystemPrefersDark());
+}
+
 export function applyTheme(theme: ThemeMode): void {
   const isDark = theme === "dark" || (theme === "system" && getSystemPrefersDark());
   document.documentElement.classList.toggle("dark", isDark);