webui: implement multi-file diff view with continuous scrolling

Replace file selector with GitHub PR-style continuous scrolling through
multiple files in a single view, improving diff navigation experience
while maintaining all Monaco editor features.

Problem Analysis:
The existing diff view required users to navigate between files using a
dropdown selector and Previous/Next buttons. This created friction when
reviewing multi-file changes and broke the natural scrolling flow that
users expect from GitHub PR views or other modern diff interfaces.

The limitation was that Monaco doesn't provide a built-in multi-file diff
widget, requiring custom implementation with multiple IStandaloneDiffEditor
instances properly configured for stacking and auto-sizing.

Implementation Changes:

1. Multi-File Layout (sketch-diff2-view.ts):
   - Replaced sketch-diff-file-picker with simple file count display
   - Implemented renderFileDiff() to create separate diff sections per file
   - Added renderFileHeader() with status badges and path information
   - Created multi-file-diff-container with vertical stacking layout
   - Added loadAllFileContents() for parallel content loading
   - Replaced single originalCode/modifiedCode with Map<string, FileContent>

2. Monaco Auto-Sizing (sketch-monaco-view.ts):
   - Configured diff editors with hidden scrollbars per Monaco ≥0.49 pattern
   - Added setupAutoSizing() with content height calculation
   - Implemented fitEditorToContent() using getContentHeight() callbacks
   - Set automaticLayout: false for manual size control
   - Added scrollbar: { vertical: 'hidden', horizontal: 'hidden', handleMouseWheel: false }
   - Enabled minimap: false and scrollBeyondLastLine: false

3. CSS Styling (sketch-diff2-view.ts):
   - Added file-diff-section with bottom borders for visual separation
   - Implemented sticky file headers with proper z-index
   - Created status badges (added, modified, deleted, renamed) with color coding
   - Added file-count display replacing old file picker interface
   - Configured diff-container with overflow: auto for outer scrolling

4. Content Management:
   - Parallel loading of all file contents with error handling
   - Maintains editability detection per file based on commit range
   - Preserves comment and save functionality for individual files
   - Updated toggleHideUnchangedRegions to apply to all editors

Technical Details:
- Uses Monaco's getContentHeight() and onDidContentSizeChange() for auto-sizing
- Each diff editor sized to Math.max(originalHeight, modifiedHeight) + 18px padding
- Outer container handles all scrolling while inner editors are sized to content
- File headers show status (Added/Modified/Deleted/Renamed) with appropriate styling
- Sticky positioning keeps file context visible during scrolling
- Maintains all existing features: editing, commenting, expand/collapse toggles

Benefits:
- Natural scrolling workflow similar to GitHub PR reviews
- Eliminates need for dropdown navigation between files
- Better visual context with file headers and status indicators
- Continuous viewing experience for multi-file changes
- Preserves all advanced Monaco features (editing, commenting, etc.)
- Improved performance with parallel content loading

Testing:
- Verified multi-file diff display with various commit ranges
- Tested scrolling behavior between files works smoothly
- Confirmed auto-sizing works correctly for different file sizes
- Validated file headers show correct status and change counts
- Ensured editing and commenting functionality preserved
- Tested expand/collapse toggles apply to all editors

This implementation follows the Monaco ≥0.49 multi-file diff pattern with
disabled inner scrollbars, auto-sizing to content, and outer scroll container,
providing a modern diff experience while maintaining full editor functionality.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s0724a00944669c80k
diff --git a/webui/src/web-components/sketch-monaco-view.ts b/webui/src/web-components/sketch-monaco-view.ts
index f472058..aaf186f 100644
--- a/webui/src/web-components/sketch-monaco-view.ts
+++ b/webui/src/web-components/sketch-monaco-view.ts
@@ -231,23 +231,21 @@
       --editor-width: 100%;
       --editor-height: 100%;
       display: flex;
-      flex: 1;
+      flex: none; /* Don't grow/shrink - size is determined by content */
       min-height: 0; /* Critical for flex layout */
       position: relative; /* Establish positioning context */
-      height: 100%; /* Take full height */
       width: 100%; /* Take full width */
+      /* Height will be set dynamically by setupAutoSizing */
     }
     main {
-      width: var(--editor-width);
-      height: var(--editor-height);
+      width: 100%;
+      height: 100%; /* Fill the host element completely */
       border: 1px solid #e0e0e0;
-      flex: 1;
-      min-height: 300px; /* Ensure a minimum height for the editor */
-      position: absolute; /* Absolute positioning to take full space */
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
+      flex: none; /* Size determined by parent */
+      min-height: 200px; /* Ensure a minimum height for the editor */
+      /* Remove absolute positioning - use normal block layout */
+      position: relative;
+      display: block;
     }
 
     /* Comment indicator and box styles */
@@ -582,6 +580,10 @@
   setOptions(value: monaco.editor.IDiffEditorConstructionOptions) {
     if (this.editor) {
       this.editor.updateOptions(value);
+      // Re-fit content after options change
+      if (this.fitEditorToContent) {
+        setTimeout(() => this.fitEditorToContent!(), 50);
+      }
     }
   }
 
@@ -598,6 +600,10 @@
           revealLineCount: 10,
         },
       });
+      // Re-fit content after toggling
+      if (this.fitEditorToContent) {
+        setTimeout(() => this.fitEditorToContent!(), 100);
+      }
     }
   }
 
@@ -619,15 +625,21 @@
 
       // First time initialization
       if (!this.editor) {
-        // Create the diff editor only once
+        // Create the diff editor with auto-sizing configuration
         this.editor = monaco.editor.createDiffEditor(this.container.value!, {
-          automaticLayout: true,
-          // Make it read-only by default
-          // We'll adjust individual editor settings after creation
+          automaticLayout: false, // We'll resize manually
           readOnly: true,
           theme: "vs", // Always use light mode
           renderSideBySide: true,
           ignoreTrimWhitespace: false,
+          scrollbar: {
+            vertical: 'hidden',
+            horizontal: 'hidden',
+            handleMouseWheel: false, // Let outer scroller eat the wheel
+          },
+          minimap: { enabled: false },
+          overviewRulerLanes: 0,
+          scrollBeyondLastLine: false,
           // Focus on the differences by hiding unchanged regions
           hideUnchangedRegions: {
             enabled: true, // Enable the feature
@@ -650,6 +662,9 @@
           this.editor.getModifiedEditor().updateOptions({ readOnly: false });
         }
 
+        // Set up auto-sizing
+        this.setupAutoSizing();
+
         // Add Monaco editor to debug global
         this.addToDebugGlobal();
       }
@@ -660,21 +675,19 @@
       this.setupContentChangeListener();
 
       // Fix cursor positioning issues by ensuring fonts are loaded
-      // This addresses the common Monaco editor cursor offset problem
       document.fonts.ready.then(() => {
         if (this.editor) {
           monaco.editor.remeasureFonts();
-          this.editor.layout();
+          this.fitEditorToContent();
         }
       });
 
       // Force layout recalculation after a short delay
-      // This ensures the editor renders properly, especially with single files
       setTimeout(() => {
         if (this.editor) {
-          this.editor.layout();
+          this.fitEditorToContent();
         }
-      }, 50);
+      }, 100);
     } catch (error) {
       console.error("Error initializing Monaco editor:", error);
     }
@@ -1067,6 +1080,21 @@
           original: this.originalModel,
           modified: this.modifiedModel,
         });
+        
+        // Set initial hideUnchangedRegions state (default to enabled/collapsed)
+        this.editor.updateOptions({
+          hideUnchangedRegions: {
+            enabled: true, // Default to collapsed state
+            contextLineCount: 3,
+            minimumLineCount: 3,
+            revealLineCount: 10,
+          },
+        });
+        
+        // Fit content after setting new models
+        if (this.fitEditorToContent) {
+          setTimeout(() => this.fitEditorToContent!(), 50);
+        }
       }
       this.setupContentChangeListener();
     } catch (error) {
@@ -1086,12 +1114,13 @@
       if (this.editor) {
         this.updateModels();
 
-        // Force layout recalculation after model updates
+        // Force auto-sizing after model updates
+        // Use a slightly longer delay to ensure layout is stable
         setTimeout(() => {
-          if (this.editor) {
-            this.editor.layout();
+          if (this.fitEditorToContent) {
+            this.fitEditorToContent();
           }
-        }, 50);
+        }, 100);
       } else {
         // If the editor isn't initialized yet but we received content,
         // initialize it now
@@ -1100,27 +1129,72 @@
     }
   }
 
+  // Set up auto-sizing for multi-file diff view
+  private setupAutoSizing() {
+    if (!this.editor) return;
+
+    const fitContent = () => {
+      try {
+        const originalEditor = this.editor!.getOriginalEditor();
+        const modifiedEditor = this.editor!.getModifiedEditor();
+        
+        const originalHeight = originalEditor.getContentHeight();
+        const modifiedHeight = modifiedEditor.getContentHeight();
+        
+        // Use the maximum height of both editors, plus some padding
+        const maxHeight = Math.max(originalHeight, modifiedHeight) + 18; // 1 blank line bottom padding
+        
+        // Set both container and host height to enable proper scrolling
+        if (this.container.value) {
+          // Set explicit heights on both container and host
+          this.container.value.style.height = `${maxHeight}px`;
+          this.style.height = `${maxHeight}px`; // Update host element height
+          
+          // Emit the height change event BEFORE calling layout
+          // This ensures parent containers resize first
+          this.dispatchEvent(new CustomEvent('monaco-height-changed', {
+            detail: { height: maxHeight },
+            bubbles: true,
+            composed: true
+          }));
+          
+          // Layout after both this component and parents have updated
+          setTimeout(() => {
+            if (this.editor && this.container.value) {
+              // Use explicit dimensions to ensure Monaco uses full available space
+              const width = this.container.value.offsetWidth;
+              this.editor.layout({
+                width: width,
+                height: maxHeight
+              });
+            }
+          }, 10);
+        }
+      } catch (error) {
+        console.error('Error in fitContent:', error);
+      }
+    };
+
+    // Store the fit function for external access
+    this.fitEditorToContent = fitContent;
+
+    // Set up listeners for content size changes
+    this.editor.getOriginalEditor().onDidContentSizeChange(fitContent);
+    this.editor.getModifiedEditor().onDidContentSizeChange(fitContent);
+
+    // Initial fit
+    fitContent();
+  }
+
+  private fitEditorToContent: (() => void) | null = null;
+
   // Add resize observer to ensure editor resizes when container changes
   firstUpdated() {
     // Initialize the editor
     this.initializeEditor();
 
-    // Create a ResizeObserver to monitor container size changes
-    if (window.ResizeObserver) {
-      const resizeObserver = new ResizeObserver(() => {
-        if (this.editor) {
-          this.editor.layout();
-        }
-      });
-
-      // Start observing the container
-      if (this.container.value) {
-        resizeObserver.observe(this.container.value);
-      }
-
-      // Store the observer for cleanup
-      this._resizeObserver = resizeObserver;
-    }
+    // For multi-file diff, we don't use ResizeObserver since we control the size
+    // Instead, we rely on auto-sizing based on content
 
     // If editable, set up edit mode and content change listener
     if (this.editableRight && this.editor) {
@@ -1209,11 +1283,14 @@
         this.modifiedModel = undefined;
       }
 
-      // Clean up resize observer
+      // Clean up resize observer (if any)
       if (this._resizeObserver) {
         this._resizeObserver.disconnect();
         this._resizeObserver = null;
       }
+      
+      // Clear the fit function reference
+      this.fitEditorToContent = null;
 
       // Remove document click handler if set
       if (this._documentClickHandler) {