Browse Source

Sujith :) ->
1. Converted Arbitrator to use MVC

sujith 1 month ago
parent
commit
9cbb9a89bd

+ 289 - 0
Assets/Arbitrator/Editor/GUI/ArbitratorController.cs

@@ -0,0 +1,289 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// This script acts as the Controller for the ArbitratorWindow. It manages all
+// state and business logic, separating it from the UI rendering code in the window.
+
+using System;
+using System.Linq;
+using UnityEditor;
+using Terra.Arbitrator.Settings;
+using Terra.Arbitrator.Services;
+using Terra.Arbitrator.Promises;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Terra.Arbitrator.GUI
+{
+    public class ArbitratorController
+    {
+        // --- Private State ---
+        private List<GitChange> _changes = new();
+        private string _infoMessage;
+        private string _errorMessage;
+        private bool _isLoading;
+        private string _loadingMessage = "";
+        
+        // --- Public Properties for the View ---
+        public IReadOnlyList<GitChange> Changes => _changes;
+        public string InfoMessage => _infoMessage;
+        public string ErrorMessage => _errorMessage;
+        public bool IsLoading => _isLoading;
+        public string LoadingMessage => _loadingMessage;
+
+        // --- Communication with the View ---
+        private readonly Action _requestRepaint;
+        private readonly Func<string, string, bool> _displayDialog;
+
+        /// <summary>
+        /// Initializes the controller.
+        /// </summary>
+        /// <param name="requestRepaint">A callback to the window's Repaint() method.</param>
+        /// <param name="displayDialog">A callback to EditorUtility.DisplayDialog for user confirmations.</param>
+        public ArbitratorController(Action requestRepaint, Func<string, string, bool> displayDialog)
+        {
+            _requestRepaint = requestRepaint;
+            _displayDialog = displayDialog;
+        }
+
+        // --- Public Methods (called by the View) ---
+
+        public void OnEnable()
+        {
+            // This logic is moved from ArbitratorWindow.OnEnable
+            if (SessionState.GetBool(BetterGitStatePersistence.ResetQueueKey, false))
+            {
+                SessionState.EraseString(BetterGitStatePersistence.ResetQueueKey);
+                _infoMessage = "Multi-file reset complete. Pulling again to confirm...";
+                Pull();
+            }
+            else
+            {
+                Refresh();
+            }
+        }
+
+        public void Refresh()
+        {
+            StartOperation("Refreshing status...");
+            
+            UnstageStep()
+                .Then(CompareStep)
+                .Then(FinalizeRefresh)
+                .Catch(HandleOperationError)
+                .Finally(FinishOperation);
+        }
+        
+        public void Pull()
+        {
+            if (_isLoading) return;
+            StartOperation("Analyzing for conflicts...");
+
+            GitService.AnalyzePullConflicts()
+                .Then(analysisResult =>
+                {
+                    if (analysisResult.HasConflicts)
+                    {
+                        // This part is synchronous UI, so it stays.
+                        FinishOperation(); // Stop loading while modal is open
+                        ConflictResolutionWindow.ShowWindow(analysisResult.ConflictingFiles, filesToActOn =>
+                        {
+                            if (filesToActOn == null) return; 
+                            if (filesToActOn.Any()) ResetMultipleFiles(filesToActOn);
+                            else ForcePull(); 
+                        });
+                    }
+                    else
+                    {
+                        StartOperation("No conflicts found. Pulling changes...");
+                        GitService.PerformSafePull()
+                            .Then(successMessage =>
+                            {
+                                _infoMessage = successMessage;
+                                // Refresh will handle its own start/finish states
+                                Refresh();
+                            })
+                            .Catch(ex => {
+                                // If the pull fails, we need to handle the error and finish
+                                HandleOperationError(ex);
+                                FinishOperation();
+                            });
+                    }
+                })
+                .Catch(ex => {
+                    HandleOperationError(ex);
+                    FinishOperation();
+                });
+        }
+
+        public void CommitAndPush(string commitMessage)
+        {   
+            var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList();
+            
+            var username = BetterGitSettings.Username;
+            var email = BetterGitSettings.Email;
+            
+            StartOperation("Staging, committing, and pushing files...");
+
+            GitService.CommitAndPush(selectedFiles, commitMessage, username, email)
+                .Then(successMessage => {
+                    _infoMessage = successMessage;
+                    Refresh();
+                })
+                .Catch(ex => {
+                    HandleOperationError(ex);
+                    FinishOperation();
+                });
+        }
+
+        public void ResetFile(GitChange change)
+        {
+            if (_isLoading) return;
+            var userConfirmed = _displayDialog("Confirm Reset", $"Are you sure you want to revert all local changes to '{change.FilePath}'? This action cannot be undone.");
+            if (!userConfirmed) return;
+            
+            StartOperation($"Resetting {change.FilePath}...");
+            
+            GitService.ResetFileChanges(change)
+                .Then(successMessage => {
+                    _infoMessage = successMessage;
+                    Refresh();
+                })
+                .Catch(ex => {
+                    HandleOperationError(ex);
+                    FinishOperation();
+                });
+        }
+        
+        public void DiffFile(GitChange change)
+        {
+            if (_isLoading) return;
+            StartOperation($"Launching diff for {change.FilePath}...");
+            
+            GitService.LaunchExternalDiff(change)
+                .Catch(HandleOperationError)
+                .Finally(Refresh);
+        }
+        
+        public void ResolveConflict(GitChange change)
+        {
+            if (_isLoading) return;
+            StartOperation($"Opening merge tool for {change.FilePath}...");
+            
+            GitService.LaunchMergeTool(change)
+                .Then(successMessage => { _infoMessage = successMessage; })
+                .Catch(HandleOperationError)
+                .Finally(Refresh);
+        }
+
+        public void ResetAll()
+        {
+            var userConfirmed = _displayDialog(
+                "Confirm Reset All",
+                "Are you sure you want to discard ALL local changes (modified, added, and untracked files)?\n\nThis action cannot be undone.");
+
+            if (!userConfirmed) return;
+
+            StartOperation("Discarding all local changes...");
+
+            GitService.ResetAllChanges()
+                .Then(successMessage =>
+                {
+                    _infoMessage = successMessage;
+                })
+                .Catch(HandleOperationError)
+                .Finally(Refresh);
+        }
+
+        public void SetAllSelection(bool selected)
+        {
+            if (_changes == null) return;
+            foreach (var change in _changes)
+            {
+                if (change.Status != LibGit2Sharp.ChangeKind.Conflicted)
+                {
+                   change.IsSelectedForCommit = selected;
+                }
+            }
+        }
+
+        // --- Private Methods ---
+        
+        private static IPromise<bool> UnstageStep()
+        {
+            return GitService.UnstageAllFilesIfSafe();
+        }
+
+        private IPromise<List<GitChange>> CompareStep(bool wasUnstaged)
+        {
+            if (wasUnstaged)
+            {
+                _infoMessage = "Found and unstaged files for review.";
+            }
+            return GitService.CompareLocalToRemote();
+        }
+
+        private void FinalizeRefresh(List<GitChange> changes)
+        {
+            _changes = changes;
+            if (string.IsNullOrEmpty(_infoMessage) && (_changes == null || _changes.Count == 0))
+            {
+                _infoMessage = "You are up-to-date! No local changes detected.";
+            }
+        }
+        
+        // --- Shared Helper Methods ---
+
+        private void StartOperation(string loadingMessage)
+        {
+            _isLoading = true;
+            _loadingMessage = loadingMessage;
+            ClearMessages();
+            _changes = null;
+            _requestRepaint?.Invoke();
+        }
+
+        private void HandleOperationError(Exception ex)
+        {
+            _errorMessage = $"Operation Failed: {ex.Message}";
+        }
+        
+        private void FinishOperation()
+        {
+            _isLoading = false;
+            _requestRepaint?.Invoke();
+        }
+        
+        private void ClearMessages()
+        {
+            _errorMessage = null;
+            _infoMessage = null;
+        }
+
+        private void ResetMultipleFiles(List<string> filePaths)
+        {
+            _infoMessage = $"Starting reset for {filePaths.Count} file(s)... This may trigger script compilation.";
+            _requestRepaint?.Invoke();
+            SessionState.SetString("BetterGit.ResetQueue", string.Join(";", filePaths));
+            EditorApplication.delayCall += BetterGitStatePersistence.ContinueInterruptedReset;
+        }
+        
+        private void ForcePull()
+        {
+            StartOperation("Attempting to pull and create conflicts...");
+            
+            EditorApplication.LockReloadAssemblies();
+            GitService.ForcePull()
+                .Then(_ =>
+                {
+                    _infoMessage = "Pull resulted in conflicts. Please resolve them below.";
+                })
+                .Catch(HandleOperationError)
+                .Finally(() =>
+                {
+                    EditorApplication.UnlockReloadAssemblies();
+                    AssetDatabase.Refresh();
+                    Refresh();
+                });
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/GUI/ArbitratorController.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: df5a3c42d94c4befafb5f5f249d592cf
+timeCreated: 1750749947

+ 111 - 384
Assets/Arbitrator/Editor/GUI/ArbitratorWindow.cs

@@ -1,33 +1,20 @@
 // Copyright (c) 2025 TerraByte Inc.
 //
-// This script creates a custom Unity Editor window called "Arbitrator" to compare 
-// local Git changes with the tracked remote branch using the LibGit2Sharp library.
-//
-// HOW TO USE:
-// 1. Ensure you have manually installed the LibGit2Sharp v0.27.0 package.
-// 2. Create an "Editor" folder in your Assets directory if you don't have one.
-// 3. Save this script as "Arbitrator.cs" inside the "Editor" folder.
-// 4. In Unity, open the window from the top menu: Terra > Arbitrator.
-// 5. Click the "Compare with Cloud" button. Results will appear in the console.
+// This script is the View component for the Better Git tool. Its only
+// responsibility is to draw the UI based on the state provided by the
+// ArbitratorController.
 
 using System.Linq;
 using UnityEngine;
 using UnityEditor;
-using Terra.Arbitrator.Services;
-using Terra.Arbitrator.Settings;
-using System.Collections.Generic;
 
 namespace Terra.Arbitrator.GUI
 {
     public class ArbitratorWindow : EditorWindow
     {
-        private List<GitChange> _changes;
+        private ArbitratorController _controller;
         private string _commitMessage = "";
         private Vector2 _scrollPosition;
-        private string _infoMessage;
-        private string _errorMessage;
-        private bool _isLoading;
-        private string _loadingMessage = "";
         private GUIStyle _evenRowStyle;
         private bool _stylesInitialized;
 
@@ -38,39 +25,24 @@ namespace Terra.Arbitrator.GUI
             window.titleContent = new GUIContent("Better Git", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image);
         }
         
-        /// <summary>
-        /// Called when the window is enabled. This triggers an automatic refresh
-        /// when the window is opened or after scripts are recompiled.
-        /// </summary>
         private void OnEnable()
         {
-            // Check if we just finished a reset operation and need to show a message.
-            if (SessionState.GetBool(BetterGitStatePersistence.ResetQueueKey, false))
-            {
-                SessionState.EraseString(BetterGitStatePersistence.ResetQueueKey);
-                _infoMessage = "Multi-file reset complete. Pulling again to confirm...";
-                // After a successful reset, we should automatically try to pull again.
-                HandlePull();
-            }
-            else
-            {
-                HandleCompare();
-            }
+            // The window creates the controller and passes delegates for communication.
+            _controller = new ArbitratorController(
+                requestRepaint: Repaint,
+                displayDialog: (heading, message) => EditorUtility.DisplayDialog(heading, message, "Yes", "No")
+            );
+            
+            _controller.OnEnable();
         }
         
-        /// <summary>
-        /// Initializes custom GUIStyles. We do this here to avoid creating new
-        /// styles and textures on every OnGUI call, which is inefficient.
-        /// </summary>
         private void InitializeStyles()
         {
             if (_stylesInitialized) return;
 
             _evenRowStyle = new GUIStyle();
             
-            // Create a 1x1 texture with a subtle gray color
             var texture = new Texture2D(1, 1);
-            // Use a slightly different color depending on the editor skin (light/dark)
             var color = EditorGUIUtility.isProSkin 
                 ? new Color(0.3f, 0.3f, 0.3f, 0.3f) 
                 : new Color(0.8f, 0.8f, 0.8f, 0.5f);
@@ -85,403 +57,145 @@ namespace Terra.Arbitrator.GUI
         private void OnGUI()
         {
             InitializeStyles();
-            
-            // --- Top Toolbar ---
             DrawToolbar();
-            
             EditorGUILayout.Space();
-
-            // --- Message Display Area ---
-            if (!string.IsNullOrEmpty(_errorMessage))
-            {
-                EditorGUILayout.HelpBox(_errorMessage, MessageType.Error);
-            }
-            else if (!string.IsNullOrEmpty(_infoMessage) && !_isLoading)
-            {
-                // Only show an info message if not loading, to prevent flicker
-                EditorGUILayout.HelpBox(_infoMessage, MessageType.Info);
-            }
-
-            // --- Main Content ---
-            else if (!_isLoading && _changes is { Count: > 0 })
-            {
-                DrawChangesList();
-                DrawCommitSection();
-            }
-        }
-        
-        private void ClearMessages()
-        {
-            _errorMessage = null;
-            _infoMessage = null;
+            DrawMessageArea();
+            DrawMainContent();
         }
         
-        /// <summary>
-        /// Draws the top menu bar for actions like refreshing.
-        /// </summary>
         private void DrawToolbar()
         {
             EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
-            EditorGUI.BeginDisabledGroup(_isLoading);
-            // The refresh button is now on the toolbar
-            if (GUILayout.Button(new GUIContent("Refresh", EditorGUIUtility.IconContent("Refresh").image, "Fetches the latest status from the remote."), EditorStyles.toolbarButton, GUILayout.Width(80)))
+            EditorGUI.BeginDisabledGroup(_controller.IsLoading);
+
+            if (GUILayout.Button(new GUIContent("Refresh", EditorGUIUtility.IconContent("Refresh").image), EditorStyles.toolbarButton, GUILayout.Width(80)))
             {
-                HandleCompare();
+                _controller.Refresh();
             }
 
-            if (GUILayout.Button(new GUIContent("Pull", EditorGUIUtility.IconContent("CollabPull").image, "Fetch and merge changes from the remote."), EditorStyles.toolbarButton, GUILayout.Width(80)))
+            if (GUILayout.Button(new GUIContent("Pull", EditorGUIUtility.IconContent("CollabPull").image), EditorStyles.toolbarButton, GUILayout.Width(80)))
             {
-                HandlePull();
+                _controller.Pull();
             }
             
-            if (_changes is { Count: > 0 })
+            if (_controller.Changes is { Count: > 0 })
             {
                 if (GUILayout.Button("Select All", EditorStyles.toolbarButton, GUILayout.Width(80)))
                 {
-                    SetAllSelection(true);
+                    _controller.SetAllSelection(true);
                 }
                 if (GUILayout.Button("Deselect All", EditorStyles.toolbarButton, GUILayout.Width(80)))
                 {
-                    SetAllSelection(false);
+                    _controller.SetAllSelection(false);
                 }
-                if (GUILayout.Button(new GUIContent("Reset All", EditorGUIUtility.IconContent("d_TreeEditor.Trash").image, "Discard ALL local changes."), EditorStyles.toolbarButton, GUILayout.Width(100)))
+            
+                GUILayout.FlexibleSpace();
+
+                var noChanges = !_controller.Changes.Any();
+                EditorGUI.BeginDisabledGroup(noChanges);
+                var originalColor = UnityEngine.GUI.backgroundColor;
+                UnityEngine.GUI.backgroundColor = new Color(1f, 0.5f, 0.5f, 0.8f);
+
+                if (GUILayout.Button(new GUIContent("Reset All", EditorGUIUtility.IconContent("d_TreeEditor.Trash").image), EditorStyles.toolbarButton, GUILayout.Width(100)))
                 {
-                    // We use delayCall to avoid GUI layout issues from the dialog.
-                    EditorApplication.delayCall += HandleResetAll;
+                    EditorApplication.delayCall += _controller.ResetAll;
                 }
-            }
             
+                UnityEngine.GUI.backgroundColor = originalColor;
+                EditorGUI.EndDisabledGroup();
+            }
+
             EditorGUI.EndDisabledGroup();
             
-            // This pushes everything that comes after it to the right.
-            GUILayout.FlexibleSpace();
-
-            // The loading message will appear on the right side of the toolbar.
-            if (_isLoading)
+            if (_controller.IsLoading)
             {
-                GUILayout.Label(_loadingMessage);
+                GUILayout.FlexibleSpace();
+                GUILayout.Label(_controller.LoadingMessage);
             }
-            
-            // Future: Add a dropdown menu for filters or settings here.
-            // If (GUILayout.Button("Filters", EditorStyles.toolbarDropDown)) { ... }
 
             EditorGUILayout.EndHorizontal();
         }
         
-        private void SetAllSelection(bool selected)
+        private void DrawMessageArea()
         {
-            if (_changes == null) return;
-            foreach (var change in _changes)
+            if (!string.IsNullOrEmpty(_controller.ErrorMessage))
             {
-                change.IsSelectedForCommit = selected;
+                EditorGUILayout.HelpBox(_controller.ErrorMessage, MessageType.Error);
             }
-        }
-
-        private void HandleCompare()
-        {
-            _isLoading = true;
-            _loadingMessage = "Comparing with remote repository...";
-            ClearMessages();
-            _changes = null;
-            
-            // First, unstage any files if it's safe to do so.
-            GitService.UnstageAllFilesIfSafe()
-                .Then(wasUnstaged =>
-                {
-                    if(wasUnstaged)
-                    {
-                        _infoMessage = "Found and unstaged files for review.";
-                    }
-                    
-                    GitService.CompareLocalToRemote()
-                        .Then(result =>
-                        {
-                            _changes = result;
-                            if (string.IsNullOrEmpty(_infoMessage) && (_changes == null || _changes.Count == 0))
-                            {
-                                _infoMessage = "You are up-to-date! No local changes detected.";
-                            }
-                        })
-                        .Catch(ex =>
-                        {
-                            _errorMessage = $"Comparison Failed: {ex.Message}";
-                        })
-                        .Finally(() =>
-                        {
-                            // This finally block only runs after the *inner* promise is done.
-                            _isLoading = false;
-                            Repaint();
-                        });
-                })
-                .Catch(ex =>
-                {
-                    // This catch block handles errors from the Unstage operation.
-                    _errorMessage = $"Unstaging Failed: {ex.Message}";
-                    _isLoading = false;
-                    Repaint();
-                });
-        }
-        
-        /// <summary>
-        /// Handles the logic for the Pull button.
-        /// </summary>
-        private void HandlePull()
-        {
-            if (_isLoading) return;
-            _isLoading = true;
-            _loadingMessage = "Analyzing for conflicts...";
-            ClearMessages();
-            Repaint();
-
-            GitService.AnalyzePullConflicts()
-                .Then(analysisResult =>
-                {
-                    if (analysisResult.HasConflicts)
-                    {
-                        // Conflicts found! Open the resolution window.
-                        _isLoading = false; // Stop loading while modal is open
-                        Repaint();
-                        ConflictResolutionWindow.ShowWindow(analysisResult.ConflictingFiles, filesToActOn =>
-                        {
-                            if (filesToActOn == null) return; // User cancelled
-                            if (filesToActOn.Any()) HandleResetMultipleFiles(filesToActOn);
-                            else HandleForcePull(); // User chose "Pull Anyway" (empty list signal)
-                        });
-                    }
-                    else
-                    {
-                        // No conflicts, proceed with a safe pull.
-                        _loadingMessage = "No conflicts found. Pulling changes...";
-                        Repaint();
-                        GitService.PerformSafePull()
-                            .Then(successMessage =>
-                            {
-                                _infoMessage = successMessage;
-                                EditorUtility.DisplayDialog("Pull Complete", successMessage, "OK");
-                                HandleCompare(); // Refresh view after successful pull
-                            })
-                            .Catch(ex =>
-                            {
-                                _errorMessage = $"Pull Failed: {ex.Message}";
-                                _isLoading = false;
-                                Repaint();
-                            });
-                    }
-                })
-                .Catch(ex =>
-                {
-                    _errorMessage = $"Pull Analysis Failed: {ex.Message}";
-                    _isLoading = false;
-                    Repaint();
-                });
-        }
-        
-        private void HandleResetMultipleFiles(List<string> filePaths)
-        {
-            _infoMessage = $"Starting reset for {filePaths.Count} file(s)... This may trigger script compilation.";
-            Repaint();
-            SessionState.SetString("BetterGit.ResetQueue", string.Join(";", filePaths));
-            EditorApplication.delayCall += BetterGitStatePersistence.ContinueInterruptedReset;
-        }
-        
-        private void HandleResetAll()
-        {
-            if (_changes == null || _changes.Count == 0)
+            else if (!string.IsNullOrEmpty(_controller.InfoMessage) && !_controller.IsLoading)
             {
-                EditorUtility.DisplayDialog("No Changes", "There are no local changes to reset.", "OK");
-                return;
+                EditorGUILayout.HelpBox(_controller.InfoMessage, MessageType.Info);
             }
-
-            var userConfirmed = EditorUtility.DisplayDialog(
-                "Confirm Reset All",
-                "Are you sure you want to discard ALL local changes (modified, added, and untracked files)?\n\nThis action cannot be undone.",
-                "Yes, Discard Everything",
-                "Cancel");
-
-            if (!userConfirmed) return;
-
-            _isLoading = true;
-            _loadingMessage = "Discarding all local changes...";
-            ClearMessages();
-            Repaint();
-
-            GitService.ResetAllChanges()
-                .Then(successMessage => {
-                    _infoMessage = successMessage;
-                    HandleCompare(); // Refresh the view to confirm everything is clean
-                })
-                .Catch(ex => {
-                    _errorMessage = $"Reset All Failed: {ex.Message}";
-                    _isLoading = false;
-                    Repaint();
-                });
-        }
-        
-        private void HandleForcePull()
-        {
-            _isLoading = true;
-            _loadingMessage = "Attempting to pull and create conflicts...";
-            ClearMessages();
-            Repaint();
-            EditorApplication.LockReloadAssemblies();
-            GitService.ForcePull()
-                .Then(_ =>
-                {
-                    _infoMessage = "Pull resulted in conflicts. Please resolve them below.";
-                    HandleCompare();
-                })
-                .Catch(ex =>
-                {
-                    _errorMessage = $"Forced Pull Failed: {ex.Message}";
-                    _isLoading = false;
-                    Repaint();
-                })
-                .Finally(() =>
-                {
-                    EditorApplication.UnlockReloadAssemblies();
-                    AssetDatabase.Refresh();
-                });
-        }
-
-        private void HandleCommitAndPush()
-        {
-            _isLoading = true;
-            _loadingMessage = "Staging, committing, and pushing files...";
-            ClearMessages();
-
-            var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList();
-            
-            var username = BetterGitSettings.Username;
-            var email = BetterGitSettings.Email;
-
-            GitService.CommitAndPush(selectedFiles, _commitMessage, username, email)
-                .Then(successMessage => {
-                    _commitMessage = ""; // Clear message on success
-                    _changes = null; // Clear the list, forcing a refresh
-                    HandleCompare(); // Automatically refresh to confirm
-                    EditorUtility.DisplayDialog("Commit and Push Complete", successMessage, "OK");
-                })
-                .Catch(ex => {
-                    _errorMessage = $"Push Failed: {ex.Message}";
-                })
-                .Finally(() => {
-                    _isLoading = false;
-                    // Repaint is handled by the chained HandleCompare call on success
-                    if (!string.IsNullOrEmpty(_errorMessage)) Repaint();
-                });
-        }
-        
-        private void HandleResetFile(GitChange change)
-        {
-            if (_isLoading) return;
-            var userConfirmed = EditorUtility.DisplayDialog("Confirm Reset", $"Are you sure you want to revert all local changes to '{change.FilePath}'? This action cannot be undone.", "Yes, Revert", "Cancel");
-            if (!userConfirmed) return;
-            _isLoading = true;
-            _loadingMessage = $"Resetting {change.FilePath}...";
-            ClearMessages();
-            Repaint();
-            GitService.ResetFileChanges(change)
-                .Then(successMessage => {
-                    _infoMessage = successMessage;
-                    HandleCompare();
-                })
-                .Catch(ex => {
-                    _errorMessage = $"Reset Failed: {ex.Message}";
-                    _isLoading = false;
-                    Repaint();
-                });
         }
         
-        private void HandleDiffFile(GitChange change)
+        private void DrawMainContent()
         {
-            if (_isLoading) return;
-            _isLoading = true;
-            _loadingMessage = $"Launching diff for {change.FilePath}...";
-            ClearMessages();
-            Repaint();
-            GitService.LaunchExternalDiff(change)
-                .Catch(ex => _errorMessage = ex.Message)
-                .Finally(() => { _isLoading = false; HandleCompare(); });
-        }
-        
-        private void HandleResolveConflict(GitChange change)
-        {
-            if (_isLoading) return;
-            _isLoading = true;
-            _loadingMessage = $"Opening merge tool for {change.FilePath}...";
-            ClearMessages();
-            Repaint();
-            
-            GitService.LaunchMergeTool(change)
-                .Then(successMessage =>
-                {
-                    _infoMessage = successMessage;
-                })
-                .Catch(ex =>
-                {
-                    _errorMessage = $"Resolve Failed: {ex.Message}";
-                })
-                .Finally(() =>
-                {
-                    _isLoading = false;
-                    HandleCompare(); // Always refresh to show the new state
-                });
+            if (_controller.IsLoading)
+            {
+                GUILayout.FlexibleSpace();
+                EditorGUILayout.BeginHorizontal();
+                GUILayout.FlexibleSpace();
+                GUILayout.Label("Loading...", EditorStyles.largeLabel);
+                GUILayout.FlexibleSpace();
+                EditorGUILayout.EndHorizontal();
+                GUILayout.FlexibleSpace();
+            }
+            else if (_controller.Changes != null && _controller.Changes.Any())
+            {
+                DrawChangesList();
+                DrawCommitSection();
+            }
+            else
+            {
+                EditorGUILayout.HelpBox("You are up-to-date! No local changes detected.", MessageType.Info);
+            }
         }
 
-        /// <summary>
-        /// Draws the multi-column list of changed files.
-        /// </summary>
         private void DrawChangesList()
         {
-            // --- Draw Header ---
             EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
-            var isConflictMode = _changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted);
-            if (!isConflictMode) EditorGUILayout.LabelField("Commit", GUILayout.Width(45));
-            else GUILayout.Space(45); // Keep layout consistent
+            EditorGUILayout.LabelField("Commit", GUILayout.Width(45));
             EditorGUILayout.LabelField("Status", GUILayout.Width(50));
             EditorGUILayout.LabelField("File Path");
-            EditorGUILayout.LabelField("Actions", GUILayout.Width(55));
+            EditorGUILayout.LabelField("Actions", GUILayout.Width(130));
             EditorGUILayout.EndHorizontal();
 
-            // --- Draw Scrollable List ---
             _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true));
-            for (var i = 0; i < _changes.Count; i++)
+            for (var i = 0; i < _controller.Changes.Count; i++)
             {
-                var change = _changes[i];
-                // Use the evenRowStyle for every second row (i % 2 == 0), otherwise use no style.
+                var change = _controller.Changes[i];
                 var rowStyle = i % 2 == 0 ? _evenRowStyle : GUIStyle.none;
                 
                 EditorGUILayout.BeginHorizontal(rowStyle);
 
-                change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45));
-                
+                if (change.Status == LibGit2Sharp.ChangeKind.Conflicted)
+                {
+                    EditorGUI.BeginDisabledGroup(true);
+                    EditorGUILayout.Toggle(false, GUILayout.Width(45));
+                    EditorGUI.EndDisabledGroup();
+                }
+                else
+                {
+                    change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45));
+                }
+
                 string status;
                 Color statusColor;
-                string filePathDisplay;
 
                 switch (change.Status)
                 {
-                    case LibGit2Sharp.ChangeKind.Added: 
-                        status = "[+]"; statusColor = Color.green; filePathDisplay = change.FilePath; break;
-                    case LibGit2Sharp.ChangeKind.Deleted: 
-                        status = "[-]"; statusColor = Color.red; filePathDisplay = change.FilePath; break;
-                    case LibGit2Sharp.ChangeKind.Modified: 
-                        status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); filePathDisplay = change.FilePath; break;
-                    case LibGit2Sharp.ChangeKind.Renamed: 
-                        status = "[R]"; statusColor = new Color(0.6f, 0.6f, 1.0f);
-                        filePathDisplay = $"{change.OldFilePath} -> {change.FilePath}"; break;
-                    case LibGit2Sharp.ChangeKind.Conflicted:
-                        status = "[C]"; statusColor = Color.magenta; filePathDisplay = change.FilePath; break;
-                    case LibGit2Sharp.ChangeKind.Unmodified:
-                    case LibGit2Sharp.ChangeKind.Copied:
-                    case LibGit2Sharp.ChangeKind.Ignored:
-                    case LibGit2Sharp.ChangeKind.Untracked:
-                    case LibGit2Sharp.ChangeKind.TypeChanged:
-                    case LibGit2Sharp.ChangeKind.Unreadable:
-                    default: 
-                        status = "[?]"; statusColor = Color.white; filePathDisplay = change.FilePath; break;
+                    case LibGit2Sharp.ChangeKind.Added: status = "[+]"; statusColor = Color.green; break;
+                    case LibGit2Sharp.ChangeKind.Deleted: status = "[-]"; statusColor = Color.red; break;
+                    case LibGit2Sharp.ChangeKind.Modified: status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); break;
+                    case LibGit2Sharp.ChangeKind.Renamed: status = "[R]"; statusColor = new Color(0.6f, 0.6f, 1.0f); break;
+                    case LibGit2Sharp.ChangeKind.Conflicted: status = "[C]"; statusColor = Color.magenta; break;
+                    default: status = "[?]"; statusColor = Color.white; break;
                 }
                 
+                var filePathDisplay = change.Status == LibGit2Sharp.ChangeKind.Renamed
+                    ? $"{change.OldFilePath} -> {change.FilePath}"
+                    : change.FilePath;
+
                 var originalColor = UnityEngine.GUI.color;
                 UnityEngine.GUI.color = statusColor;
                 EditorGUILayout.LabelField(new GUIContent(status, change.Status.ToString()), GUILayout.Width(50));
@@ -489,20 +203,31 @@ namespace Terra.Arbitrator.GUI
 
                 EditorGUILayout.LabelField(new GUIContent(filePathDisplay, filePathDisplay));
 
-                EditorGUI.BeginDisabledGroup(_isLoading);
-                if (status == "[C]")
+                EditorGUILayout.BeginHorizontal(GUILayout.Width(120));
+                EditorGUI.BeginDisabledGroup(_controller.IsLoading);
+
+                if (change.Status == LibGit2Sharp.ChangeKind.Conflicted)
                 {
-                    if (GUILayout.Button("Resolve", GUILayout.Width(70))) { EditorApplication.delayCall += () => HandleResolveConflict(change); }
+                    if (GUILayout.Button("Resolve", GUILayout.Width(70)))
+                    {
+                        EditorApplication.delayCall += () => _controller.ResolveConflict(change);
+                    }
                 }
                 else
                 {
-                    if (GUILayout.Button("Diff", GUILayout.Width(45))) EditorApplication.delayCall += () => HandleDiffFile(change);
+                    if (GUILayout.Button("Diff", GUILayout.Width(45)))
+                    {
+                        EditorApplication.delayCall += () => _controller.DiffFile(change);
+                    }
                 }
+                
                 if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55)))
                 {
-                    EditorApplication.delayCall += () => HandleResetFile(change);
+                    EditorApplication.delayCall += () => _controller.ResetFile(change);
                 }
+                
                 EditorGUI.EndDisabledGroup();
+                EditorGUILayout.EndHorizontal();
                 
                 EditorGUILayout.EndHorizontal();
             }
@@ -512,21 +237,23 @@ namespace Terra.Arbitrator.GUI
         private void DrawCommitSection()
         {
             EditorGUILayout.Space(10);
-            var isConflictMode = _changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted);
-            if (isConflictMode)
+            if (_controller.Changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted))
             {
                 EditorGUILayout.HelpBox("You must resolve all conflicts before you can commit.", MessageType.Warning);
                 return;
             }
+            
             EditorGUILayout.LabelField("Commit & Push", EditorStyles.boldLabel);
             _commitMessage = EditorGUILayout.TextArea(_commitMessage, GUILayout.Height(60), GUILayout.ExpandWidth(true));
 
-            var isPushDisabled = string.IsNullOrWhiteSpace(_commitMessage) || !_changes.Any(c => c.IsSelectedForCommit);
+            var isPushDisabled = string.IsNullOrWhiteSpace(_commitMessage) || !_controller.Changes.Any(c => c.IsSelectedForCommit);
 
             EditorGUI.BeginDisabledGroup(isPushDisabled);
             if (GUILayout.Button("Commit & Push Selected Files", GUILayout.Height(40)))
             {
-                HandleCommitAndPush();
+                // Pass the UI state to the controller.
+                _controller.CommitAndPush(_commitMessage);
+                _commitMessage = ""; // Clear on sending
             }
             EditorGUI.EndDisabledGroup();
             

+ 7 - 0
Assets/Arbitrator/Editor/Promises/IPromise.cs

@@ -17,6 +17,13 @@ namespace Terra.Arbitrator.Promises
         /// </summary>
         IPromise<TResult> Then(Action<TResult> onResolved);
         
+        /// <summary>
+        /// Attaches a function that returns a new promise, allowing operations to be chained.
+        /// The chain will wait until the returned promise is resolved before continuing.
+        /// </summary>
+        /// <returns>A new promise that resolves with the result of the chained promise.</returns>
+        IPromise<TNewResult> Then<TNewResult>(Func<TResult, IPromise<TNewResult>> onResolved);
+        
         /// <summary>
         /// Attaches a callback that will execute when the promise is rejected.
         /// </summary>

+ 24 - 2
Assets/Arbitrator/Editor/Promises/Promise.cs

@@ -14,12 +14,10 @@ namespace Terra.Arbitrator.Promises
         private TResult _resolvedValue;
         private Exception _rejectedException;
 
-        // Callbacks to be executed on the main thread
         private Action<TResult> _onResolvedCallback;
         private Action<Exception> _onRejectedCallback;
         private Action _onFinallyCallback;
         
-        // Thread-safe flags
         private volatile bool _isResolved;
         private volatile bool _isRejected;
 
@@ -68,6 +66,30 @@ namespace Terra.Arbitrator.Promises
             _onResolvedCallback = onResolved;
             return this;
         }
+        
+        public IPromise<TNewResult> Then<TNewResult>(Func<TResult, IPromise<TNewResult>> onResolved)
+        {
+            return new Promise<TNewResult>(LocalExecutor);
+
+            void LocalExecutor(Action<TNewResult> resolve, Action<Exception> reject)
+            {
+                Then(OnResolved).Catch(reject);
+                return;
+
+                void OnResolved(TResult result)
+                {
+                    try
+                    {
+                        var nextPromise = onResolved(result);
+                        nextPromise.Then(resolve).Catch(reject);
+                    }
+                    catch (Exception ex)
+                    {
+                        reject(ex);
+                    }
+                }
+            }
+        }
 
         public IPromise<TResult> Catch(Action<Exception> onRejected)
         {