chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class CliInstallRefreshPolicy
|
||||
{
|
||||
internal static bool ShouldRefreshSkillsAfterCliInstall(bool wasCliInstalledBeforeInstall)
|
||||
{
|
||||
return !wasCliInstalledBeforeInstall;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e244a03db91a449e0b0b0aed8f1a7abd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class CliVersionComparison
|
||||
{
|
||||
internal static bool TryCompare(string leftVersionText, string rightVersionText, out int comparison)
|
||||
{
|
||||
comparison = 0;
|
||||
|
||||
if (!TryParse(leftVersionText, out CliSemanticVersion leftVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParse(rightVersionText, out CliSemanticVersion rightVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comparison = Compare(leftVersion, rightVersion);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsSameVersion(string leftVersionText, string rightVersionText)
|
||||
{
|
||||
if (!TryCompare(leftVersionText, rightVersionText, out int comparison))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return comparison == 0;
|
||||
}
|
||||
|
||||
private static bool TryParse(string versionText, out CliSemanticVersion version)
|
||||
{
|
||||
version = default;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(versionText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string normalized = versionText.Trim().TrimStart('v', 'V');
|
||||
int buildMetadataIndex = normalized.IndexOf('+');
|
||||
if (buildMetadataIndex >= 0)
|
||||
{
|
||||
normalized = normalized.Substring(0, buildMetadataIndex);
|
||||
}
|
||||
|
||||
string preRelease = string.Empty;
|
||||
int preReleaseIndex = normalized.IndexOf('-');
|
||||
if (preReleaseIndex >= 0)
|
||||
{
|
||||
preRelease = normalized.Substring(preReleaseIndex + 1);
|
||||
normalized = normalized.Substring(0, preReleaseIndex);
|
||||
}
|
||||
|
||||
if (preReleaseIndex >= 0 && !IsValidPreRelease(preRelease))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] components = normalized.Split('.');
|
||||
if (components.Length != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseComponent(components[0], out int major))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseComponent(components[1], out int minor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseComponent(components[2], out int patch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
version = new CliSemanticVersion(major, minor, patch, preRelease);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseComponent(string component, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (string.IsNullOrEmpty(component))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return int.TryParse(component, NumberStyles.None, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
private static bool IsValidPreRelease(string preRelease)
|
||||
{
|
||||
if (string.IsNullOrEmpty(preRelease))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] identifiers = preRelease.Split('.');
|
||||
foreach (string identifier in identifiers)
|
||||
{
|
||||
if (!IsValidPreReleaseIdentifier(identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidPreReleaseIdentifier(string identifier)
|
||||
{
|
||||
if (string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool numericOnly = true;
|
||||
foreach (char character in identifier)
|
||||
{
|
||||
bool isDigit = character >= '0' && character <= '9';
|
||||
bool isUpper = character >= 'A' && character <= 'Z';
|
||||
bool isLower = character >= 'a' && character <= 'z';
|
||||
bool isHyphen = character == '-';
|
||||
if (!isDigit && !isUpper && !isLower && !isHyphen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
numericOnly = numericOnly && isDigit;
|
||||
}
|
||||
|
||||
return !numericOnly || identifier.Length == 1 || identifier[0] != '0';
|
||||
}
|
||||
|
||||
private static int Compare(CliSemanticVersion left, CliSemanticVersion right)
|
||||
{
|
||||
int majorComparison = left.Major.CompareTo(right.Major);
|
||||
if (majorComparison != 0)
|
||||
{
|
||||
return majorComparison;
|
||||
}
|
||||
|
||||
int minorComparison = left.Minor.CompareTo(right.Minor);
|
||||
if (minorComparison != 0)
|
||||
{
|
||||
return minorComparison;
|
||||
}
|
||||
|
||||
int patchComparison = left.Patch.CompareTo(right.Patch);
|
||||
if (patchComparison != 0)
|
||||
{
|
||||
return patchComparison;
|
||||
}
|
||||
|
||||
return ComparePreRelease(left, right);
|
||||
}
|
||||
|
||||
private static int ComparePreRelease(CliSemanticVersion left, CliSemanticVersion right)
|
||||
{
|
||||
if (!left.HasPreRelease && !right.HasPreRelease)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!left.HasPreRelease)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!right.HasPreRelease)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
string[] leftIdentifiers = left.PreRelease.Split('.');
|
||||
string[] rightIdentifiers = right.PreRelease.Split('.');
|
||||
int maxLength = Math.Max(leftIdentifiers.Length, rightIdentifiers.Length);
|
||||
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
{
|
||||
if (i >= leftIdentifiers.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (i >= rightIdentifiers.Length)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int identifierComparison = ComparePreReleaseIdentifier(leftIdentifiers[i], rightIdentifiers[i]);
|
||||
if (identifierComparison != 0)
|
||||
{
|
||||
return identifierComparison;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int ComparePreReleaseIdentifier(string left, string right)
|
||||
{
|
||||
bool leftIsNumeric = int.TryParse(left, NumberStyles.None, CultureInfo.InvariantCulture, out int leftNumber);
|
||||
bool rightIsNumeric = int.TryParse(right, NumberStyles.None, CultureInfo.InvariantCulture, out int rightNumber);
|
||||
|
||||
if (leftIsNumeric && rightIsNumeric)
|
||||
{
|
||||
return leftNumber.CompareTo(rightNumber);
|
||||
}
|
||||
|
||||
if (leftIsNumeric)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (rightIsNumeric)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return string.Compare(left, right, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private readonly struct CliSemanticVersion
|
||||
{
|
||||
internal readonly int Major;
|
||||
internal readonly int Minor;
|
||||
internal readonly int Patch;
|
||||
internal readonly string PreRelease;
|
||||
|
||||
internal CliSemanticVersion(int major, int minor, int patch, string preRelease)
|
||||
{
|
||||
Major = major;
|
||||
Minor = minor;
|
||||
Patch = patch;
|
||||
PreRelease = preRelease;
|
||||
}
|
||||
|
||||
internal bool HasPreRelease => !string.IsNullOrEmpty(PreRelease);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 984ecd7223ecf2e41b1f37a7662ef824
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public static class EditorDialogHelper
|
||||
{
|
||||
public static void ShowSkillsInstalledDialog()
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
EditorDialog.DisplayAlertDialog(
|
||||
"Skills Installed",
|
||||
"Skills have been installed successfully.",
|
||||
"OK",
|
||||
DialogIconType.Info);
|
||||
#else
|
||||
EditorUtility.DisplayDialog(
|
||||
"Skills Installed",
|
||||
"Skills have been installed successfully.",
|
||||
"OK");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 768573908011d4cdfac43e8e64888f7a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,286 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// An entry for the MCP communication log.
|
||||
/// Log information that includes a request/response pair.
|
||||
/// </summary>
|
||||
public record McpCommunicationLogEntry
|
||||
{
|
||||
public readonly string CommandName;
|
||||
public readonly DateTime Timestamp;
|
||||
public readonly string Request;
|
||||
public readonly string Response;
|
||||
public readonly bool IsError;
|
||||
public bool IsExpanded { get; set; }
|
||||
|
||||
public string HeaderText => $"[{CommandName}: {Timestamp:HH:mm:ss}]";
|
||||
|
||||
public McpCommunicationLogEntry(string commandName, DateTime timestamp, string request, string response, bool isError, bool isExpanded = false)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Timestamp = timestamp;
|
||||
Request = request;
|
||||
Response = response;
|
||||
IsError = isError;
|
||||
IsExpanded = isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class for managing MCP communication logs.
|
||||
/// </summary>
|
||||
public static class McpCommunicationLogger
|
||||
{
|
||||
// Note: Communication logs are now managed via McpSessionManager
|
||||
|
||||
private static List<McpCommunicationLogEntry> _logs;
|
||||
private static Dictionary<string, PendingRequest> _pendingRequests;
|
||||
|
||||
/// <summary>
|
||||
/// Event for when the log is updated (for UI updates).
|
||||
/// </summary>
|
||||
public static event Action OnLogUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Static constructor (automatically executed after a domain reload).
|
||||
/// </summary>
|
||||
static McpCommunicationLogger()
|
||||
{
|
||||
LoadFromSessionState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about pending requests.
|
||||
/// </summary>
|
||||
private record PendingRequest
|
||||
{
|
||||
public readonly string CommandName;
|
||||
public readonly DateTime Timestamp;
|
||||
public readonly string RequestJson;
|
||||
|
||||
public PendingRequest(string commandName, DateTime timestamp, string requestJson)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Timestamp = timestamp;
|
||||
RequestJson = requestJson;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all logs.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<McpCommunicationLogEntry> GetAllLogs()
|
||||
{
|
||||
lock (_logs)
|
||||
{
|
||||
return new List<McpCommunicationLogEntry>(_logs).AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a request (awaiting response).
|
||||
/// </summary>
|
||||
public static async Task LogRequest(string jsonRequest)
|
||||
{
|
||||
// Skip if communication logs are disabled
|
||||
if (!McpEditorSettings.GetSettings().enableCommunicationLogs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
JObject request = JObject.Parse(jsonRequest);
|
||||
string method = request["method"]?.ToString() ?? "unknown";
|
||||
string id = NormalizeId(request["id"]);
|
||||
|
||||
PendingRequest pendingRequest = new(method, DateTime.Now, jsonRequest);
|
||||
|
||||
// Store in memory immediately (on any thread)
|
||||
lock (_pendingRequests)
|
||||
{
|
||||
_pendingRequests[id] = pendingRequest;
|
||||
}
|
||||
|
||||
// Switch to main thread for SessionState operations
|
||||
await MainThreadSwitcher.SwitchToMainThread();
|
||||
|
||||
SaveToSessionState();
|
||||
OnLogUpdated?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a response (adds it to the log paired with its request).
|
||||
/// </summary>
|
||||
public static async Task RecordLogResponse(string jsonResponse)
|
||||
{
|
||||
// Skip if communication logs are disabled
|
||||
if (!McpEditorSettings.GetSettings().enableCommunicationLogs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
JObject response = JObject.Parse(jsonResponse);
|
||||
string id = NormalizeId(response["id"]);
|
||||
|
||||
PendingRequest pendingRequest;
|
||||
bool foundPendingRequest;
|
||||
|
||||
// Process response in memory immediately (on any thread)
|
||||
lock (_pendingRequests)
|
||||
{
|
||||
foundPendingRequest = _pendingRequests.TryGetValue(id, out pendingRequest);
|
||||
if (foundPendingRequest)
|
||||
{
|
||||
_pendingRequests.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPendingRequest)
|
||||
{
|
||||
bool isError = response["error"] != null;
|
||||
|
||||
// Create new logs in a closed state.
|
||||
McpCommunicationLogEntry logEntry = new(
|
||||
pendingRequest.CommandName,
|
||||
pendingRequest.Timestamp,
|
||||
pendingRequest.RequestJson,
|
||||
jsonResponse,
|
||||
isError,
|
||||
false // Start with the toggle closed.
|
||||
);
|
||||
|
||||
lock (_logs)
|
||||
{
|
||||
// If there are existing logs, close them all (before adding a new log).
|
||||
foreach (McpCommunicationLogEntry existingLog in _logs)
|
||||
{
|
||||
existingLog.IsExpanded = false;
|
||||
}
|
||||
|
||||
_logs.Add(logEntry);
|
||||
|
||||
// Remove old entries if exceeding maximum
|
||||
while (_logs.Count > McpUIConstants.MAX_COMMUNICATION_LOG_ENTRIES)
|
||||
{
|
||||
_logs.RemoveAt(0); // Remove oldest entry
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to main thread for SessionState operations
|
||||
await MainThreadSwitcher.SwitchToMainThread();
|
||||
|
||||
SaveToSessionState();
|
||||
OnLogUpdated?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all logs.
|
||||
/// </summary>
|
||||
public static void ClearLogs()
|
||||
{
|
||||
lock (_logs)
|
||||
{
|
||||
_logs.Clear();
|
||||
}
|
||||
|
||||
lock (_pendingRequests)
|
||||
{
|
||||
_pendingRequests.Clear();
|
||||
}
|
||||
|
||||
// Also completely delete from SessionState and execute UI update on the main thread.
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
ClearLogSessionState();
|
||||
OnLogUpdated?.Invoke();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores data from SessionState.
|
||||
/// </summary>
|
||||
private static void LoadFromSessionState()
|
||||
{
|
||||
// Restore logs.
|
||||
string logsJson = McpEditorSettings.GetCommunicationLogsJson();
|
||||
_logs = JsonConvert.DeserializeObject<List<McpCommunicationLogEntry>>(logsJson) ?? new List<McpCommunicationLogEntry>();
|
||||
|
||||
// Restore pending requests.
|
||||
string pendingJson = McpEditorSettings.GetPendingRequestsJson();
|
||||
_pendingRequests = JsonConvert.DeserializeObject<Dictionary<string, PendingRequest>>(pendingJson) ?? new Dictionary<string, PendingRequest>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves data to SessionState.
|
||||
/// </summary>
|
||||
public static void SaveToSessionState()
|
||||
{
|
||||
// Create snapshots to avoid collection modification during serialization
|
||||
List<McpCommunicationLogEntry> logsSnapshot;
|
||||
Dictionary<string, PendingRequest> pendingSnapshot;
|
||||
|
||||
lock (_logs)
|
||||
{
|
||||
logsSnapshot = new List<McpCommunicationLogEntry>(_logs);
|
||||
}
|
||||
|
||||
lock (_pendingRequests)
|
||||
{
|
||||
pendingSnapshot = new Dictionary<string, PendingRequest>(_pendingRequests);
|
||||
}
|
||||
|
||||
string logsJson = JsonConvert.SerializeObject(logsSnapshot);
|
||||
string pendingJson = JsonConvert.SerializeObject(pendingSnapshot);
|
||||
|
||||
McpEditorSettings.SetCommunicationLogsJson(logsJson);
|
||||
McpEditorSettings.SetPendingRequestsJson(pendingJson);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears SessionState related to communication logs (for fixing log issues).
|
||||
/// </summary>
|
||||
public static void ClearLogSessionState()
|
||||
{
|
||||
McpEditorSettings.ClearCommunicationLogs();
|
||||
|
||||
lock (_logs)
|
||||
{
|
||||
_logs?.Clear();
|
||||
}
|
||||
|
||||
lock (_pendingRequests)
|
||||
{
|
||||
_pendingRequests?.Clear();
|
||||
}
|
||||
|
||||
// SessionState clear complete (no log output).
|
||||
|
||||
// Notify UI of update.
|
||||
EditorApplication.delayCall += () => OnLogUpdated?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize ID to consistent string representation for proper matching
|
||||
/// </summary>
|
||||
private static string NormalizeId(JToken idToken)
|
||||
{
|
||||
if (idToken == null)
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// Convert both string and numeric IDs to string for consistent matching
|
||||
return idToken.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 347cfd0ba9b6c45208b43ca1de434e6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,399 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Model layer for McpEditorWindow in MVP architecture
|
||||
/// Handles state management and business logic using immutable state objects
|
||||
/// Related classes:
|
||||
/// - McpEditorWindowState: State objects managed by this model
|
||||
/// - McpEditorWindow: Presenter that uses this model
|
||||
/// - McpEditorWindowView: View layer for UI rendering
|
||||
/// - McpEditorSettings: Persistent settings storage
|
||||
/// </summary>
|
||||
public class McpEditorModel
|
||||
{
|
||||
public UIState UI { get; private set; }
|
||||
public RuntimeState Runtime { get; private set; }
|
||||
|
||||
public McpEditorModel()
|
||||
{
|
||||
UI = new UIState();
|
||||
Runtime = new RuntimeState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update UI state with new values
|
||||
/// </summary>
|
||||
/// <param name="updater">Function to update UI state</param>
|
||||
public void UpdateUIState(Func<UIState, UIState> updater)
|
||||
{
|
||||
UI = updater(UI);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update runtime state with new values
|
||||
/// </summary>
|
||||
/// <param name="updater">Function to update runtime state</param>
|
||||
public void UpdateRuntimeState(Func<RuntimeState, RuntimeState> updater)
|
||||
{
|
||||
Runtime = updater(Runtime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load state from persistent settings
|
||||
/// </summary>
|
||||
public void LoadFromSettings()
|
||||
{
|
||||
McpEditorSettingsData settings = McpEditorSettings.GetSettings();
|
||||
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: settings.customPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: settings.showSecuritySettings,
|
||||
showToolSettings: settings.showToolSettings,
|
||||
addRepositoryRoot: settings.addRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: (ConnectionMode)settings.connectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current UI state to persistent settings
|
||||
/// </summary>
|
||||
public void SaveToSettings()
|
||||
{
|
||||
McpEditorSettings.SetCustomPort(UI.CustomPort);
|
||||
McpEditorSettings.SetConnectionMode(UI.ConnectionMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load state from persistent settings (formerly from SessionState)
|
||||
/// </summary>
|
||||
public void LoadFromSessionState()
|
||||
{
|
||||
McpEditorType selectedEditor = McpEditorSettings.GetSelectedEditorType();
|
||||
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: selectedEditor,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current state to persistent settings (formerly to SessionState)
|
||||
/// </summary>
|
||||
public void SaveToSessionState()
|
||||
{
|
||||
McpEditorSettings.SetSelectedEditorType(UI.SelectedEditorType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize post-compile mode
|
||||
/// </summary>
|
||||
public void EnablePostCompileMode()
|
||||
{
|
||||
UpdateRuntimeState(runtime => new RuntimeState(
|
||||
isPostCompileMode: true,
|
||||
needsRepaint: true,
|
||||
lastServerRunning: runtime.LastServerRunning,
|
||||
lastServerPort: runtime.LastServerPort,
|
||||
lastConnectedClientsCount: runtime.LastConnectedClientsCount,
|
||||
lastClientsInfoHash: runtime.LastClientsInfoHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exit post-compile mode
|
||||
/// </summary>
|
||||
public void DisablePostCompileMode()
|
||||
{
|
||||
UpdateRuntimeState(runtime => new RuntimeState(
|
||||
isPostCompileMode: false,
|
||||
needsRepaint: runtime.NeedsRepaint,
|
||||
lastServerRunning: runtime.LastServerRunning,
|
||||
lastServerPort: runtime.LastServerPort,
|
||||
lastConnectedClientsCount: runtime.LastConnectedClientsCount,
|
||||
lastClientsInfoHash: runtime.LastClientsInfoHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark that UI repaint is needed
|
||||
/// </summary>
|
||||
public void RequestRepaint()
|
||||
{
|
||||
UpdateRuntimeState(runtime => new RuntimeState(
|
||||
isPostCompileMode: runtime.IsPostCompileMode,
|
||||
needsRepaint: true,
|
||||
lastServerRunning: runtime.LastServerRunning,
|
||||
lastServerPort: runtime.LastServerPort,
|
||||
lastConnectedClientsCount: runtime.LastConnectedClientsCount,
|
||||
lastClientsInfoHash: runtime.LastClientsInfoHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear repaint request
|
||||
/// </summary>
|
||||
public void ClearRepaintRequest()
|
||||
{
|
||||
UpdateRuntimeState(runtime => new RuntimeState(
|
||||
isPostCompileMode: runtime.IsPostCompileMode,
|
||||
needsRepaint: false,
|
||||
lastServerRunning: runtime.LastServerRunning,
|
||||
lastServerPort: runtime.LastServerPort,
|
||||
lastConnectedClientsCount: runtime.LastConnectedClientsCount,
|
||||
lastClientsInfoHash: runtime.LastClientsInfoHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update server state tracking for change detection
|
||||
/// </summary>
|
||||
public void UpdateServerStateTracking(bool isRunning, int port, int clientCount, string clientsHash)
|
||||
{
|
||||
UpdateRuntimeState(runtime => new RuntimeState(
|
||||
isPostCompileMode: runtime.IsPostCompileMode,
|
||||
needsRepaint: runtime.NeedsRepaint,
|
||||
lastServerRunning: isRunning,
|
||||
lastServerPort: port,
|
||||
lastConnectedClientsCount: clientCount,
|
||||
lastClientsInfoHash: clientsHash));
|
||||
}
|
||||
|
||||
// UIState-specific update methods with persistence
|
||||
|
||||
/// <summary>
|
||||
/// Update CustomPort setting with persistence
|
||||
/// </summary>
|
||||
public void UpdateCustomPort(int port)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: port,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
McpEditorSettings.SetCustomPort(port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update ShowConnectedTools setting
|
||||
/// </summary>
|
||||
public void UpdateShowConnectedTools(bool show)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: show,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update SelectedEditorType setting with persistence
|
||||
/// </summary>
|
||||
public void UpdateSelectedEditorType(McpEditorType type)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: type,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
McpEditorSettings.SetSelectedEditorType(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update MainScrollPosition setting
|
||||
/// </summary>
|
||||
public void UpdateMainScrollPosition(Vector2 position)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: position,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update ShowSecuritySettings setting with persistence
|
||||
/// </summary>
|
||||
public void UpdateShowSecuritySettings(bool show)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: show,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
McpEditorSettings.SetShowSecuritySettings(show);
|
||||
}
|
||||
|
||||
public void UpdateShowToolSettings(bool show)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: show,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
McpEditorSettings.SetShowToolSettings(show);
|
||||
}
|
||||
|
||||
public void UpdateToolEnabled(string toolName, bool enabled)
|
||||
{
|
||||
ToolSettings.SetToolEnabled(toolName, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update AllowThirdPartyTools setting with persistence
|
||||
/// </summary>
|
||||
public void UpdateAllowThirdPartyTools(bool allow)
|
||||
{
|
||||
ULoopSettings.SetAllowThirdPartyTools(allow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update AddRepositoryRoot setting with persistence
|
||||
/// </summary>
|
||||
public void UpdateAddRepositoryRoot(bool addRepositoryRoot)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: addRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
McpEditorSettings.SetAddRepositoryRoot(addRepositoryRoot);
|
||||
UnityMcpPathResolver.InvalidateGitRootCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update SupportsRepositoryRootToggle flag
|
||||
/// </summary>
|
||||
public void UpdateSupportsRepositoryRootToggle(bool supportsToggle)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: supportsToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update ShowRepositoryRootToggle flag
|
||||
/// </summary>
|
||||
public void UpdateShowRepositoryRootToggle(bool showToggle)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: showToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
}
|
||||
|
||||
public void UpdateConnectionMode(ConnectionMode mode)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: mode,
|
||||
showConfiguration: ui.ShowConfiguration));
|
||||
McpEditorSettings.SetConnectionMode(mode);
|
||||
}
|
||||
|
||||
public void UpdateShowConfiguration(bool show)
|
||||
{
|
||||
UpdateUIState(ui => new UIState(
|
||||
customPort: ui.CustomPort,
|
||||
showConnectedTools: ui.ShowConnectedTools,
|
||||
selectedEditorType: ui.SelectedEditorType,
|
||||
mainScrollPosition: ui.MainScrollPosition,
|
||||
showSecuritySettings: ui.ShowSecuritySettings,
|
||||
showToolSettings: ui.ShowToolSettings,
|
||||
addRepositoryRoot: ui.AddRepositoryRoot,
|
||||
supportsRepositoryRootToggle: ui.SupportsRepositoryRootToggle,
|
||||
showRepositoryRootToggle: ui.ShowRepositoryRootToggle,
|
||||
connectionMode: ui.ConnectionMode,
|
||||
showConfiguration: show));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa534ee6588b94345b092a151f7ab08c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab6c30c9ac75a40f09762d6481fec8ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,240 @@
|
||||
using Unity.Profiling;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Event handler for McpEditorWindow - Manages Unity and server events
|
||||
/// Helper class for Presenter layer in MVP architecture
|
||||
/// Related classes:
|
||||
/// - McpEditorWindow: Main presenter that owns this handler
|
||||
/// - McpEditorModel: Model layer for state management
|
||||
/// - McpBridgeServer: Server that provides events
|
||||
/// - McpServerController: Server lifecycle management
|
||||
/// </summary>
|
||||
internal class McpEditorWindowEventHandler
|
||||
{
|
||||
private static readonly ProfilerMarker s_onEditorUpdateMarker =
|
||||
new ProfilerMarker("McpEditorWindow.OnEditorUpdate");
|
||||
private static readonly ProfilerMarker s_refreshUiMarker =
|
||||
new ProfilerMarker("McpEditorWindow.RefreshUI");
|
||||
|
||||
private readonly McpEditorModel _model;
|
||||
private readonly McpEditorWindow _window;
|
||||
|
||||
public McpEditorWindowEventHandler(McpEditorModel model, McpEditorWindow window)
|
||||
{
|
||||
_model = model;
|
||||
_window = window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize all event subscriptions
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
SubscribeToUnityEvents();
|
||||
SubscribeToServerEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup all event subscriptions
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
UnsubscribeFromUnityEvents();
|
||||
UnsubscribeFromServerEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to Unity Editor events
|
||||
/// </summary>
|
||||
private void SubscribeToUnityEvents()
|
||||
{
|
||||
EditorApplication.update += OnEditorUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from Unity Editor events
|
||||
/// </summary>
|
||||
private void UnsubscribeFromUnityEvents()
|
||||
{
|
||||
EditorApplication.update -= OnEditorUpdate;
|
||||
}
|
||||
|
||||
private void SubscribeToServerEvents()
|
||||
{
|
||||
UnsubscribeFromServerEvents();
|
||||
|
||||
McpBridgeServer.OnServerStarted += OnServerStateChanged;
|
||||
McpBridgeServer.OnServerStopping += OnServerStateChanged;
|
||||
ConnectedToolsMonitoringService.OnConnectedToolsChanged += OnConnectedToolsChanged;
|
||||
CustomToolManager.OnToolsChanged += OnToolsChanged;
|
||||
|
||||
McpBridgeServer currentServer = McpServerController.CurrentServer;
|
||||
if (currentServer != null)
|
||||
{
|
||||
currentServer.OnClientConnected += OnClientConnected;
|
||||
currentServer.OnClientDisconnected += OnClientDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromServerEvents()
|
||||
{
|
||||
McpBridgeServer.OnServerStarted -= OnServerStateChanged;
|
||||
McpBridgeServer.OnServerStopping -= OnServerStateChanged;
|
||||
ConnectedToolsMonitoringService.OnConnectedToolsChanged -= OnConnectedToolsChanged;
|
||||
CustomToolManager.OnToolsChanged -= OnToolsChanged;
|
||||
|
||||
McpBridgeServer currentServer = McpServerController.CurrentServer;
|
||||
if (currentServer != null)
|
||||
{
|
||||
currentServer.OnClientConnected -= OnClientConnected;
|
||||
currentServer.OnClientDisconnected -= OnClientDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServerStateChanged()
|
||||
{
|
||||
_window.InvalidateToolSettingsCatalog();
|
||||
_model.RequestRepaint();
|
||||
}
|
||||
|
||||
private void OnConnectedToolsChanged()
|
||||
{
|
||||
_model.RequestRepaint();
|
||||
}
|
||||
|
||||
private void OnToolsChanged()
|
||||
{
|
||||
_window.InvalidateToolSettingsCatalog();
|
||||
_model.RequestRepaint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle client connection event - force UI repaint for immediate update
|
||||
/// </summary>
|
||||
private void OnClientConnected(string clientEndpoint)
|
||||
{
|
||||
// Enhanced logging for debugging client connection
|
||||
// Count check for debugging purposes only
|
||||
|
||||
// Clear reconnecting flags when client connects
|
||||
McpServerController.ClearReconnectingFlag();
|
||||
|
||||
// Mark that repaint is needed since events are called from background thread
|
||||
_model.RequestRepaint();
|
||||
|
||||
// Exit post-compile mode when client connects
|
||||
if (_model.Runtime.IsPostCompileMode)
|
||||
{
|
||||
_model.DisablePostCompileMode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle client disconnection event - force UI repaint for immediate update
|
||||
/// </summary>
|
||||
private void OnClientDisconnected(string clientEndpoint)
|
||||
{
|
||||
// Enhanced logging for debugging client disconnection issues
|
||||
// Count check for debugging purposes only
|
||||
|
||||
|
||||
// Mark that repaint is needed since events are called from background thread
|
||||
_model.RequestRepaint();
|
||||
}
|
||||
|
||||
private void OnEditorUpdate()
|
||||
{
|
||||
using (s_onEditorUpdateMarker.Auto())
|
||||
{
|
||||
if (!McpEditorWindowRefreshPolicy.ShouldRefreshOnEditorUpdate(_model.Runtime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_model.ClearRepaintRequest();
|
||||
using (s_refreshUiMarker.Auto())
|
||||
{
|
||||
_window.RefreshAllSections();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-subscribe to server events (called after server start)
|
||||
/// </summary>
|
||||
public void RefreshServerEventSubscriptions()
|
||||
{
|
||||
SubscribeToServerEvents();
|
||||
}
|
||||
}
|
||||
|
||||
// Post-compile recovery can stay active while UI data is unchanged, so explicit repaint
|
||||
// requests gate expensive full-section refreshes.
|
||||
internal static class McpEditorWindowRefreshPolicy
|
||||
{
|
||||
public static bool ShouldRefreshOnEditorUpdate(RuntimeState runtimeState)
|
||||
{
|
||||
Debug.Assert(runtimeState != null, "runtimeState must not be null");
|
||||
|
||||
return runtimeState.NeedsRepaint;
|
||||
}
|
||||
|
||||
public static bool ShouldRunExpensiveChecks(McpEditorWindowRefreshMode refreshMode)
|
||||
{
|
||||
return refreshMode == McpEditorWindowRefreshMode.Full;
|
||||
}
|
||||
|
||||
public static bool ShouldRefreshSkillInstallState(
|
||||
McpEditorWindowRefreshMode refreshMode,
|
||||
bool refreshRequested)
|
||||
{
|
||||
return refreshRequested && ShouldRunExpensiveChecks(refreshMode);
|
||||
}
|
||||
|
||||
public static bool ShouldKeepToolSettingsCatalogDirty(ToolSettingsSectionData toolSettingsData)
|
||||
{
|
||||
Debug.Assert(toolSettingsData != null, "toolSettingsData must not be null");
|
||||
|
||||
return toolSettingsData.ShowToolSettings && !toolSettingsData.IsRegistryAvailable;
|
||||
}
|
||||
|
||||
public static bool ShouldStartToolSettingsRegistryWarmup(
|
||||
bool isAlreadyScheduled,
|
||||
int attemptCount,
|
||||
int maxAttempts)
|
||||
{
|
||||
Debug.Assert(attemptCount >= 0, "attemptCount must not be negative");
|
||||
Debug.Assert(maxAttempts > 0, "maxAttempts must be positive");
|
||||
|
||||
return !isAlreadyScheduled && attemptCount < maxAttempts;
|
||||
}
|
||||
|
||||
public static double CalculateToolSettingsRegistryWarmupDelaySeconds(
|
||||
double initialDelaySeconds,
|
||||
double maxDelaySeconds,
|
||||
int attemptCount)
|
||||
{
|
||||
Debug.Assert(initialDelaySeconds > 0.0, "initialDelaySeconds must be positive");
|
||||
Debug.Assert(maxDelaySeconds >= initialDelaySeconds, "maxDelaySeconds must not be smaller than initialDelaySeconds");
|
||||
Debug.Assert(attemptCount >= 0, "attemptCount must not be negative");
|
||||
|
||||
double delaySeconds = initialDelaySeconds;
|
||||
for (int i = 0; i < attemptCount; i++)
|
||||
{
|
||||
delaySeconds *= 2.0;
|
||||
}
|
||||
|
||||
return System.Math.Min(delaySeconds, maxDelaySeconds);
|
||||
}
|
||||
}
|
||||
|
||||
internal enum McpEditorWindowRefreshMode
|
||||
{
|
||||
InitialPaint,
|
||||
Full
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d171c9fe3128441419f4a5b1dbc41ab2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,103 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// State management objects for McpEditorWindow
|
||||
/// Implements State Object pattern as Model layer in MVP architecture
|
||||
/// Uses get-only properties for with expression compatibility
|
||||
/// Related classes:
|
||||
/// - McpEditorWindow: Presenter layer that uses these state objects
|
||||
/// - McpEditorWindowView: View layer for UI rendering
|
||||
/// - McpEditorModel: Model layer service for managing state transitions
|
||||
/// </summary>
|
||||
|
||||
public enum ConnectionMode
|
||||
{
|
||||
CLI = 0,
|
||||
MCP = 1
|
||||
}
|
||||
|
||||
public enum SkillsTarget
|
||||
{
|
||||
Claude = 0,
|
||||
Codex = 4,
|
||||
Cursor = 2,
|
||||
Gemini = 3,
|
||||
[InspectorName("Other (.agents)")]
|
||||
Agents = 1
|
||||
}
|
||||
|
||||
public record UIState
|
||||
{
|
||||
public int CustomPort { get; }
|
||||
public bool ShowConnectedTools { get; }
|
||||
public McpEditorType SelectedEditorType { get; }
|
||||
public Vector2 MainScrollPosition { get; }
|
||||
public bool ShowSecuritySettings { get; }
|
||||
public bool ShowToolSettings { get; }
|
||||
public bool AddRepositoryRoot { get; }
|
||||
public bool SupportsRepositoryRootToggle { get; }
|
||||
public bool ShowRepositoryRootToggle { get; }
|
||||
public ConnectionMode ConnectionMode { get; }
|
||||
public bool ShowConfiguration { get; }
|
||||
|
||||
public UIState(
|
||||
int customPort = McpServerConfig.DEFAULT_PORT,
|
||||
bool showConnectedTools = true,
|
||||
McpEditorType selectedEditorType = McpEditorType.Cursor,
|
||||
Vector2 mainScrollPosition = default,
|
||||
bool showSecuritySettings = true,
|
||||
bool showToolSettings = true,
|
||||
bool addRepositoryRoot = false,
|
||||
bool supportsRepositoryRootToggle = false,
|
||||
bool showRepositoryRootToggle = false,
|
||||
ConnectionMode connectionMode = ConnectionMode.CLI,
|
||||
bool showConfiguration = true)
|
||||
{
|
||||
CustomPort = customPort;
|
||||
ShowConnectedTools = showConnectedTools;
|
||||
SelectedEditorType = selectedEditorType;
|
||||
MainScrollPosition = mainScrollPosition;
|
||||
ShowSecuritySettings = showSecuritySettings;
|
||||
ShowToolSettings = showToolSettings;
|
||||
AddRepositoryRoot = addRepositoryRoot;
|
||||
SupportsRepositoryRootToggle = supportsRepositoryRootToggle;
|
||||
ShowRepositoryRootToggle = showRepositoryRootToggle;
|
||||
ConnectionMode = connectionMode;
|
||||
ShowConfiguration = showConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime state data for McpEditorWindow
|
||||
/// Tracks dynamic state during editor window operation
|
||||
/// </summary>
|
||||
public record RuntimeState
|
||||
{
|
||||
public bool NeedsRepaint { get; }
|
||||
public bool IsPostCompileMode { get; }
|
||||
public bool LastServerRunning { get; }
|
||||
public int LastServerPort { get; }
|
||||
public int LastConnectedClientsCount { get; }
|
||||
public string LastClientsInfoHash { get; }
|
||||
|
||||
public RuntimeState(
|
||||
bool needsRepaint = false,
|
||||
bool isPostCompileMode = false,
|
||||
bool lastServerRunning = false,
|
||||
int lastServerPort = 0,
|
||||
int lastConnectedClientsCount = 0,
|
||||
string lastClientsInfoHash = "")
|
||||
{
|
||||
NeedsRepaint = needsRepaint;
|
||||
IsPostCompileMode = isPostCompileMode;
|
||||
LastServerRunning = lastServerRunning;
|
||||
LastServerPort = lastServerPort;
|
||||
LastConnectedClientsCount = lastConnectedClientsCount;
|
||||
LastClientsInfoHash = lastClientsInfoHash;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10e986461c0be463d9d0b571b5af7661
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,210 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Data structures for McpEditorWindow View rendering
|
||||
/// Related classes: McpEditorWindow, McpEditorWindowView
|
||||
/// </summary>
|
||||
|
||||
public record ServerStatusData
|
||||
{
|
||||
public readonly bool IsRunning;
|
||||
public readonly int Port;
|
||||
public readonly string Status;
|
||||
public readonly Color StatusColor;
|
||||
|
||||
public ServerStatusData(bool isRunning, int port, string status, Color statusColor)
|
||||
{
|
||||
IsRunning = isRunning;
|
||||
Port = port;
|
||||
Status = status;
|
||||
StatusColor = statusColor;
|
||||
}
|
||||
}
|
||||
|
||||
public record ServerControlsData
|
||||
{
|
||||
public readonly int CustomPort;
|
||||
public readonly bool IsServerRunning;
|
||||
public readonly bool PortEditable;
|
||||
public readonly bool HasPortWarning;
|
||||
public readonly string PortWarningMessage;
|
||||
|
||||
public ServerControlsData(int customPort, bool isServerRunning, bool portEditable, bool hasPortWarning = false, string portWarningMessage = null)
|
||||
{
|
||||
CustomPort = customPort;
|
||||
IsServerRunning = isServerRunning;
|
||||
PortEditable = portEditable;
|
||||
HasPortWarning = hasPortWarning;
|
||||
PortWarningMessage = portWarningMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public record ConnectedToolsData
|
||||
{
|
||||
public readonly IReadOnlyCollection<ConnectedClient> Clients;
|
||||
public readonly bool ShowFoldout;
|
||||
public readonly bool IsServerRunning;
|
||||
public readonly bool ShowReconnectingUI;
|
||||
public readonly bool ShowSection;
|
||||
|
||||
public ConnectedToolsData(IReadOnlyCollection<ConnectedClient> clients, bool showFoldout, bool isServerRunning, bool showReconnectingUI, bool showSection)
|
||||
{
|
||||
Clients = clients;
|
||||
ShowFoldout = showFoldout;
|
||||
IsServerRunning = isServerRunning;
|
||||
ShowReconnectingUI = showReconnectingUI;
|
||||
ShowSection = showSection;
|
||||
}
|
||||
}
|
||||
|
||||
public record EditorConfigData
|
||||
{
|
||||
public readonly McpEditorType SelectedEditor;
|
||||
public readonly bool IsServerRunning;
|
||||
public readonly int CurrentPort;
|
||||
public readonly bool IsConfigured;
|
||||
public readonly bool HasPortMismatch;
|
||||
public readonly string ConfigurationError;
|
||||
public readonly bool IsUpdateNeeded;
|
||||
public readonly bool AddRepositoryRoot;
|
||||
public readonly bool SupportsRepositoryRootToggle;
|
||||
public readonly bool ShowRepositoryRootToggle;
|
||||
public readonly bool IsChecking;
|
||||
|
||||
public EditorConfigData(McpEditorType selectedEditor, bool isServerRunning, int currentPort, bool isConfigured = false, bool hasPortMismatch = false, string configurationError = null, bool isUpdateNeeded = true, bool addRepositoryRoot = false, bool supportsRepositoryRootToggle = false, bool showRepositoryRootToggle = false, bool isChecking = false)
|
||||
{
|
||||
SelectedEditor = selectedEditor;
|
||||
IsServerRunning = isServerRunning;
|
||||
CurrentPort = currentPort;
|
||||
IsConfigured = isConfigured;
|
||||
HasPortMismatch = hasPortMismatch;
|
||||
ConfigurationError = configurationError;
|
||||
IsUpdateNeeded = isUpdateNeeded;
|
||||
AddRepositoryRoot = addRepositoryRoot;
|
||||
SupportsRepositoryRootToggle = supportsRepositoryRootToggle;
|
||||
ShowRepositoryRootToggle = showRepositoryRootToggle;
|
||||
IsChecking = isChecking;
|
||||
}
|
||||
}
|
||||
|
||||
public record ConnectionModeData
|
||||
{
|
||||
public readonly ConnectionMode Mode;
|
||||
|
||||
public ConnectionModeData(ConnectionMode mode)
|
||||
{
|
||||
Mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
public record ToolToggleItem
|
||||
{
|
||||
public readonly string ToolName;
|
||||
public readonly string Description;
|
||||
public readonly bool IsEnabled;
|
||||
public readonly bool IsThirdParty;
|
||||
|
||||
public ToolToggleItem(string toolName, string description, bool isEnabled, bool isThirdParty)
|
||||
{
|
||||
ToolName = toolName;
|
||||
Description = description;
|
||||
IsEnabled = isEnabled;
|
||||
IsThirdParty = isThirdParty;
|
||||
}
|
||||
}
|
||||
|
||||
public record ToolSettingsSectionData
|
||||
{
|
||||
public readonly bool ShowToolSettings;
|
||||
public readonly bool AllowThirdPartyTools;
|
||||
public readonly DynamicCodeSecurityLevel DynamicCodeSecurityLevel;
|
||||
public readonly ToolToggleItem[] BuiltInTools;
|
||||
public readonly ToolToggleItem[] ThirdPartyTools;
|
||||
public readonly bool IsRegistryAvailable;
|
||||
public readonly bool HasToolListData;
|
||||
|
||||
public ToolSettingsSectionData(
|
||||
bool showToolSettings,
|
||||
bool allowThirdPartyTools,
|
||||
DynamicCodeSecurityLevel dynamicCodeSecurityLevel,
|
||||
ToolToggleItem[] builtInTools,
|
||||
ToolToggleItem[] thirdPartyTools,
|
||||
bool isRegistryAvailable,
|
||||
bool hasToolListData = true)
|
||||
{
|
||||
ShowToolSettings = showToolSettings;
|
||||
AllowThirdPartyTools = allowThirdPartyTools;
|
||||
DynamicCodeSecurityLevel = dynamicCodeSecurityLevel;
|
||||
BuiltInTools = builtInTools;
|
||||
ThirdPartyTools = thirdPartyTools;
|
||||
IsRegistryAvailable = isRegistryAvailable;
|
||||
HasToolListData = hasToolListData;
|
||||
}
|
||||
}
|
||||
|
||||
public record CliSetupData
|
||||
{
|
||||
public readonly bool IsCliInstalled;
|
||||
public readonly string CliVersion;
|
||||
public readonly string PackageVersion;
|
||||
public readonly bool NeedsUpdate;
|
||||
public readonly bool NeedsDowngrade;
|
||||
public readonly bool IsInstallingCli;
|
||||
public readonly bool IsChecking;
|
||||
public readonly bool IsClaudeSkillsInstalled;
|
||||
public readonly bool IsAgentsSkillsInstalled;
|
||||
public readonly bool IsCursorSkillsInstalled;
|
||||
public readonly bool IsGeminiSkillsInstalled;
|
||||
public readonly bool IsCodexSkillsInstalled;
|
||||
public readonly bool IsAntigravitySkillsInstalled;
|
||||
public readonly SkillInstallState SelectedTargetInstallState;
|
||||
public readonly SkillsTarget SelectedTarget;
|
||||
public readonly bool GroupSkillsUnderUnityCliLoop;
|
||||
public readonly bool UseProjectCliVersion;
|
||||
public readonly bool IsInstallingSkills;
|
||||
|
||||
public CliSetupData(
|
||||
bool isCliInstalled,
|
||||
string cliVersion,
|
||||
string packageVersion,
|
||||
bool needsUpdate,
|
||||
bool needsDowngrade,
|
||||
bool isInstallingCli,
|
||||
bool isChecking,
|
||||
bool isClaudeSkillsInstalled,
|
||||
bool isAgentsSkillsInstalled,
|
||||
bool isCursorSkillsInstalled,
|
||||
bool isGeminiSkillsInstalled,
|
||||
bool isCodexSkillsInstalled,
|
||||
bool isAntigravitySkillsInstalled,
|
||||
SkillInstallState selectedTargetInstallState,
|
||||
SkillsTarget selectedTarget,
|
||||
bool groupSkillsUnderUnityCliLoop,
|
||||
bool useProjectCliVersion,
|
||||
bool isInstallingSkills)
|
||||
{
|
||||
IsCliInstalled = isCliInstalled;
|
||||
CliVersion = cliVersion;
|
||||
PackageVersion = packageVersion;
|
||||
NeedsUpdate = needsUpdate;
|
||||
NeedsDowngrade = needsDowngrade;
|
||||
IsInstallingCli = isInstallingCli;
|
||||
IsChecking = isChecking;
|
||||
IsClaudeSkillsInstalled = isClaudeSkillsInstalled;
|
||||
IsAgentsSkillsInstalled = isAgentsSkillsInstalled;
|
||||
IsCursorSkillsInstalled = isCursorSkillsInstalled;
|
||||
IsGeminiSkillsInstalled = isGeminiSkillsInstalled;
|
||||
IsCodexSkillsInstalled = isCodexSkillsInstalled;
|
||||
IsAntigravitySkillsInstalled = isAntigravitySkillsInstalled;
|
||||
SelectedTargetInstallState = selectedTargetInstallState;
|
||||
SelectedTarget = selectedTarget;
|
||||
GroupSkillsUnderUnityCliLoop = groupSkillsUnderUnityCliLoop;
|
||||
UseProjectCliVersion = useProjectCliVersion;
|
||||
IsInstallingSkills = isInstallingSkills;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c03e148d650c4ce4aa37f6269d20777
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8882470e8cb14e40a123cb1db1b8fa6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,382 @@
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class RecordingsEditorWindow : EditorWindow
|
||||
{
|
||||
private const string UXML_RELATIVE_PATH = "Editor/UI/Recordings/RecordingsEditorWindow.uxml";
|
||||
private const string USS_RELATIVE_PATH = "Editor/UI/Recordings/RecordingsEditorWindow.uss";
|
||||
|
||||
private Button _recordButton;
|
||||
private Button _replayButton;
|
||||
private Button _openFolderButton;
|
||||
private IntegerField _delayField;
|
||||
private DropdownField _fileDropdown;
|
||||
private Label _recordStatusLabel;
|
||||
private Label _replayStatusLabel;
|
||||
private VisualElement _recordStatusIndicator;
|
||||
private VisualElement _replayStatusIndicator;
|
||||
|
||||
private List<string> _recordingFiles = new();
|
||||
|
||||
private bool _prevIsRecording;
|
||||
private bool _prevIsReplaying;
|
||||
private RecordInputOverlayPhase _prevPhase;
|
||||
private int _prevMinutes = -1;
|
||||
private int _prevSecs = -1;
|
||||
private int _prevReplayFrame = -1;
|
||||
private int _countdownGeneration;
|
||||
|
||||
[MenuItem("Window/Unity CLI Loop/Recordings", priority = 1)]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
RecordingsEditorWindow window = GetWindow<RecordingsEditorWindow>("Recordings");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void CreateGUI()
|
||||
{
|
||||
string uxmlPath = $"{McpConstants.PackageAssetPath}/{UXML_RELATIVE_PATH}";
|
||||
VisualTreeAsset uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
|
||||
Debug.Assert(uxml != null, "UXML not found at " + uxmlPath);
|
||||
uxml.CloneTree(rootVisualElement);
|
||||
|
||||
string ussPath = $"{McpConstants.PackageAssetPath}/{USS_RELATIVE_PATH}";
|
||||
StyleSheet uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
|
||||
Debug.Assert(uss != null, "USS not found at " + ussPath);
|
||||
rootVisualElement.styleSheets.Add(uss);
|
||||
|
||||
QueryElements();
|
||||
BindEvents();
|
||||
RefreshFileList();
|
||||
UpdateRecordUI();
|
||||
UpdateReplayUI();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.update += OnEditorUpdate;
|
||||
InputReplayer.ReplayCompleted += OnReplayCompleted;
|
||||
InputRecorder.RecordingStopped += OnRecordingStopped;
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= OnEditorUpdate;
|
||||
InputReplayer.ReplayCompleted -= OnReplayCompleted;
|
||||
InputRecorder.RecordingStopped -= OnRecordingStopped;
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
UnbindEvents();
|
||||
}
|
||||
|
||||
private void UnbindEvents()
|
||||
{
|
||||
if (_recordButton != null) _recordButton.clicked -= OnRecordButtonClicked;
|
||||
if (_replayButton != null) _replayButton.clicked -= OnReplayButtonClicked;
|
||||
if (_openFolderButton != null) _openFolderButton.clicked -= OnOpenFolderClicked;
|
||||
}
|
||||
|
||||
private void QueryElements()
|
||||
{
|
||||
_recordButton = rootVisualElement.Q<Button>("record-button");
|
||||
_replayButton = rootVisualElement.Q<Button>("replay-button");
|
||||
_openFolderButton = rootVisualElement.Q<Button>("open-folder-button");
|
||||
_delayField = rootVisualElement.Q<IntegerField>("delay-field");
|
||||
_fileDropdown = rootVisualElement.Q<DropdownField>("file-dropdown");
|
||||
_recordStatusLabel = rootVisualElement.Q<Label>("record-status-label");
|
||||
_replayStatusLabel = rootVisualElement.Q<Label>("replay-status-label");
|
||||
_recordStatusIndicator = rootVisualElement.Q("record-status-indicator");
|
||||
_replayStatusIndicator = rootVisualElement.Q("replay-status-indicator");
|
||||
}
|
||||
|
||||
private void BindEvents()
|
||||
{
|
||||
_recordButton.clicked += OnRecordButtonClicked;
|
||||
_replayButton.clicked += OnReplayButtonClicked;
|
||||
_openFolderButton.clicked += OnOpenFolderClicked;
|
||||
}
|
||||
|
||||
private void OnRecordButtonClicked()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Recordings", "PlayMode must be active to record.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (RecordInputOverlayState.Phase == RecordInputOverlayPhase.Countdown)
|
||||
{
|
||||
_countdownGeneration++;
|
||||
RecordInputOverlayState.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (InputRecorder.IsRecording)
|
||||
{
|
||||
InputRecordingData data = InputRecorder.StopRecording();
|
||||
string outputPath = InputRecordingFileHelper.ResolveOutputPath("");
|
||||
InputRecordingFileHelper.Save(data, outputPath);
|
||||
InputRecorder.NotifyRecordingStopped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (InputReplayer.IsReplaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Recordings", "Cannot record while replaying.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
int delaySeconds = Mathf.Clamp(_delayField.value, RecordInputConstants.MIN_DELAY_SECONDS, RecordInputConstants.MAX_DELAY_SECONDS);
|
||||
|
||||
OverlayCanvasFactory.EnsureExists();
|
||||
RecordReplayOverlayFactory.EnsureRecordOverlay();
|
||||
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
int generation = ++_countdownGeneration;
|
||||
RecordInputOverlayState.StartCountdown(delaySeconds);
|
||||
int delayMs = delaySeconds * 1000;
|
||||
TimerDelay.WaitThenExecuteOnMainThread(delayMs, () =>
|
||||
{
|
||||
if (!EditorApplication.isPlaying
|
||||
|| generation != _countdownGeneration
|
||||
|| RecordInputOverlayState.Phase != RecordInputOverlayPhase.Countdown)
|
||||
{
|
||||
RecordInputOverlayState.Clear();
|
||||
return;
|
||||
}
|
||||
RecordInputOverlayState.StartRecording();
|
||||
InputRecorder.StartRecording(null);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
RecordInputOverlayState.StartRecording();
|
||||
InputRecorder.StartRecording(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReplayButtonClicked()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Recordings", "PlayMode must be active to replay.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (InputReplayer.IsReplaying)
|
||||
{
|
||||
InputReplayer.StopReplay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (InputRecorder.IsRecording)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Recordings", "Cannot replay while recording.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
string selectedFile = GetSelectedFilePath();
|
||||
if (string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Recordings", "No recording file selected.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
InputRecordingData data = InputRecordingFileHelper.Load(selectedFile);
|
||||
if (data == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Recordings", "Failed to load recording file.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
OverlayCanvasFactory.EnsureExists();
|
||||
RecordReplayOverlayFactory.EnsureReplayOverlay();
|
||||
InputReplayer.StartReplay(data, false, true);
|
||||
}
|
||||
|
||||
private void OnOpenFolderClicked()
|
||||
{
|
||||
string dir = RecordInputConstants.DEFAULT_OUTPUT_DIR;
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
EditorUtility.RevealInFinder(dir);
|
||||
}
|
||||
|
||||
private void OnRecordingStopped()
|
||||
{
|
||||
RefreshFileList();
|
||||
}
|
||||
|
||||
private void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
if (state == PlayModeStateChange.EnteredPlayMode || state == PlayModeStateChange.EnteredEditMode)
|
||||
{
|
||||
RefreshFileList();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReplayCompleted()
|
||||
{
|
||||
UpdateReplayUI();
|
||||
}
|
||||
|
||||
private void OnEditorUpdate()
|
||||
{
|
||||
bool isRecording = InputRecorder.IsRecording;
|
||||
bool isReplaying = InputReplayer.IsReplaying;
|
||||
RecordInputOverlayPhase phase = RecordInputOverlayState.Phase;
|
||||
|
||||
bool stateChanged = isRecording != _prevIsRecording
|
||||
|| isReplaying != _prevIsReplaying
|
||||
|| phase != _prevPhase;
|
||||
|
||||
if (!isRecording && !isReplaying && phase == RecordInputOverlayPhase.None && !stateChanged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_prevIsRecording = isRecording;
|
||||
_prevIsReplaying = isReplaying;
|
||||
_prevPhase = phase;
|
||||
|
||||
UpdateRecordUI();
|
||||
UpdateReplayUI();
|
||||
}
|
||||
|
||||
private void UpdateRecordUI()
|
||||
{
|
||||
if (_recordButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isRecording = InputRecorder.IsRecording;
|
||||
RecordInputOverlayPhase phase = RecordInputOverlayState.Phase;
|
||||
|
||||
if (isRecording)
|
||||
{
|
||||
_recordButton.text = "Stop Recording";
|
||||
_recordButton.RemoveFromClassList("rec-button--record");
|
||||
_recordButton.AddToClassList("rec-button--recording");
|
||||
float elapsed = RecordInputOverlayState.ElapsedSeconds;
|
||||
int minutes = (int)(elapsed / 60f);
|
||||
int secs = (int)(elapsed % 60f);
|
||||
if (minutes != _prevMinutes || secs != _prevSecs)
|
||||
{
|
||||
_prevMinutes = minutes;
|
||||
_prevSecs = secs;
|
||||
_recordStatusLabel.text = $"Recording {minutes:D2}:{secs:D2}";
|
||||
}
|
||||
SetIndicatorClass(_recordStatusIndicator, "rec-status-indicator--recording");
|
||||
}
|
||||
else if (phase == RecordInputOverlayPhase.Countdown)
|
||||
{
|
||||
_recordButton.text = "Cancel";
|
||||
_recordButton.RemoveFromClassList("rec-button--recording");
|
||||
_recordButton.AddToClassList("rec-button--record");
|
||||
int remaining = Mathf.CeilToInt(RecordInputOverlayState.RemainingSeconds);
|
||||
_recordStatusLabel.text = $"Starting in {remaining}...";
|
||||
SetIndicatorClass(_recordStatusIndicator, "rec-status-indicator--countdown");
|
||||
}
|
||||
else
|
||||
{
|
||||
_recordButton.text = "Start Recording";
|
||||
_recordButton.RemoveFromClassList("rec-button--recording");
|
||||
_recordButton.AddToClassList("rec-button--record");
|
||||
_recordStatusLabel.text = "Idle";
|
||||
SetIndicatorClass(_recordStatusIndicator, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateReplayUI()
|
||||
{
|
||||
if (_replayButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isReplaying = InputReplayer.IsReplaying;
|
||||
|
||||
if (isReplaying)
|
||||
{
|
||||
_replayButton.text = "Stop Replay";
|
||||
_replayButton.RemoveFromClassList("rec-button--replay");
|
||||
_replayButton.AddToClassList("rec-button--replaying");
|
||||
int current = InputReplayer.CurrentFrame;
|
||||
int total = InputReplayer.TotalFrames;
|
||||
if (current != _prevReplayFrame)
|
||||
{
|
||||
_prevReplayFrame = current;
|
||||
_replayStatusLabel.text = $"Replay {current} / {total}";
|
||||
}
|
||||
SetIndicatorClass(_replayStatusIndicator, "rec-status-indicator--replaying");
|
||||
}
|
||||
else
|
||||
{
|
||||
_replayButton.text = "Start Replay";
|
||||
_replayButton.RemoveFromClassList("rec-button--replaying");
|
||||
_replayButton.AddToClassList("rec-button--replay");
|
||||
_replayStatusLabel.text = "Idle";
|
||||
SetIndicatorClass(_replayStatusIndicator, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshFileList()
|
||||
{
|
||||
_recordingFiles.Clear();
|
||||
|
||||
string dir = RecordInputConstants.DEFAULT_OUTPUT_DIR;
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
string[] files = Directory.GetFiles(dir, "*.json");
|
||||
Array.Sort(files);
|
||||
Array.Reverse(files);
|
||||
_recordingFiles.AddRange(files);
|
||||
}
|
||||
|
||||
List<string> displayNames = _recordingFiles.Select(Path.GetFileName).ToList();
|
||||
|
||||
if (_fileDropdown != null)
|
||||
{
|
||||
_fileDropdown.choices = displayNames;
|
||||
if (displayNames.Count > 0)
|
||||
{
|
||||
_fileDropdown.index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSelectedFilePath()
|
||||
{
|
||||
if (_fileDropdown == null || _fileDropdown.index < 0 || _fileDropdown.index >= _recordingFiles.Count)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return _recordingFiles[_fileDropdown.index];
|
||||
}
|
||||
|
||||
private static void SetIndicatorClass(VisualElement indicator, string activeClass)
|
||||
{
|
||||
indicator.RemoveFromClassList("rec-status-indicator--recording");
|
||||
indicator.RemoveFromClassList("rec-status-indicator--replaying");
|
||||
indicator.RemoveFromClassList("rec-status-indicator--countdown");
|
||||
|
||||
if (!string.IsNullOrEmpty(activeClass))
|
||||
{
|
||||
indicator.AddToClassList(activeClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2c023a74a2ad48699e777e630eb7ce1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,111 @@
|
||||
:root {
|
||||
--rec-color-record: #ef5350;
|
||||
--rec-color-replay: #42a5f5;
|
||||
--rec-color-idle: #9e9e9e;
|
||||
--rec-color-active: #4caf50;
|
||||
}
|
||||
|
||||
.rec-main-container {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.rec-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rec-section--box {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rec-section__header {
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Row */
|
||||
.rec-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rec-row__label {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rec-row__int-field {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.rec-row__dropdown {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.rec-button {
|
||||
height: 24px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.rec-button--record {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.rec-button--replay {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.rec-button--recording {
|
||||
background-color: var(--rec-color-record);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rec-button--replaying {
|
||||
background-color: var(--rec-color-replay);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Status */
|
||||
.rec-status-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.rec-status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--rec-color-idle);
|
||||
}
|
||||
|
||||
.rec-status-indicator--recording {
|
||||
background-color: var(--rec-color-record);
|
||||
}
|
||||
|
||||
.rec-status-indicator--replaying {
|
||||
background-color: var(--rec-color-replay);
|
||||
}
|
||||
|
||||
.rec-status-indicator--countdown {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.rec-status-label {
|
||||
font-size: 11px;
|
||||
color: var(--unity-colors-default-text);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51d87d9f7c7a14772b42fa0fb2df0278
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
<ui:ScrollView name="main-scroll-view" class="rec-main-container">
|
||||
|
||||
<!-- Record Section -->
|
||||
<ui:VisualElement name="record-section" class="rec-section rec-section--box">
|
||||
<ui:Label text="Record" class="rec-section__header" />
|
||||
|
||||
<ui:VisualElement class="rec-row">
|
||||
<ui:Label text="Delay (sec):" class="rec-row__label" />
|
||||
<uie:IntegerField name="delay-field" value="3" class="rec-row__int-field" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="rec-row">
|
||||
<ui:Button name="record-button" text="Start Recording" class="rec-button rec-button--record" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="rec-status-row">
|
||||
<ui:VisualElement name="record-status-indicator" class="rec-status-indicator" />
|
||||
<ui:Label name="record-status-label" text="Idle" class="rec-status-label" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Replay Section -->
|
||||
<ui:VisualElement name="replay-section" class="rec-section rec-section--box">
|
||||
<ui:Label text="Replay" class="rec-section__header" />
|
||||
|
||||
<ui:VisualElement class="rec-row">
|
||||
<ui:Label text="File:" class="rec-row__label" />
|
||||
<ui:DropdownField name="file-dropdown" class="rec-row__dropdown" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="rec-row">
|
||||
<ui:Button name="open-folder-button" text="Open Folder" class="rec-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="rec-row">
|
||||
<ui:Button name="replay-button" text="Start Replay" class="rec-button rec-button--replay" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="rec-status-row">
|
||||
<ui:VisualElement name="replay-status-indicator" class="rec-status-indicator" />
|
||||
<ui:Label name="replay-status-label" text="Idle" class="rec-status-label" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
</ui:ScrollView>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f7b2604575c14efe8a88c27791da3fd
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24cf07fd4b0ab49e090fda64d942f8bc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,202 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Standalone window for server status monitoring and lifecycle control.
|
||||
/// Accessible from Window > Unity CLI Loop > Server.
|
||||
/// </summary>
|
||||
public class ServerEditorWindow : EditorWindow
|
||||
{
|
||||
private const string UXML_RELATIVE_PATH = "Editor/UI/Server/ServerEditorWindow.uxml";
|
||||
private const string USS_RELATIVE_PATH = "Editor/UI/Server/ServerEditorWindow.uss";
|
||||
|
||||
private ServerStatusSection _serverStatusSection;
|
||||
private ServerControlsSection _serverControlsSection;
|
||||
private volatile bool _needsRepaint;
|
||||
|
||||
[MenuItem("Window/Unity CLI Loop/Server", priority = 2)]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
ServerEditorWindow window = GetWindow<ServerEditorWindow>("Server");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.update += OnEditorUpdate;
|
||||
McpBridgeServer.OnServerStarted += OnServerStateChanged;
|
||||
McpBridgeServer.OnServerStopping += OnServerStateChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= OnEditorUpdate;
|
||||
McpBridgeServer.OnServerStarted -= OnServerStateChanged;
|
||||
McpBridgeServer.OnServerStopping -= OnServerStateChanged;
|
||||
}
|
||||
|
||||
private void CreateGUI()
|
||||
{
|
||||
LoadLayout();
|
||||
InitializeSections();
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void LoadLayout()
|
||||
{
|
||||
string uxmlPath = $"{McpConstants.PackageAssetPath}/{UXML_RELATIVE_PATH}";
|
||||
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
|
||||
Debug.Assert(visualTree != null, $"UXML not found at {uxmlPath}");
|
||||
visualTree.CloneTree(rootVisualElement);
|
||||
|
||||
string ussPath = $"{McpConstants.PackageAssetPath}/{USS_RELATIVE_PATH}";
|
||||
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
|
||||
Debug.Assert(styleSheet != null, $"USS not found at {ussPath}");
|
||||
rootVisualElement.styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
private void InitializeSections()
|
||||
{
|
||||
_serverStatusSection = new ServerStatusSection(rootVisualElement);
|
||||
|
||||
_serverControlsSection = new ServerControlsSection(rootVisualElement);
|
||||
_serverControlsSection.OnToggleServer += ToggleServer;
|
||||
_serverControlsSection.OnPortChanged += OnPortChanged;
|
||||
}
|
||||
|
||||
private void RefreshUI()
|
||||
{
|
||||
if (_serverStatusSection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SyncPortIfRunning();
|
||||
|
||||
ServerStatusData statusData = CreateServerStatusData();
|
||||
_serverStatusSection.Update(statusData);
|
||||
|
||||
ServerControlsData controlsData = CreateServerControlsData();
|
||||
_serverControlsSection.Update(controlsData);
|
||||
}
|
||||
|
||||
private void SyncPortIfRunning()
|
||||
{
|
||||
if (!McpServerController.IsServerRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int actualPort = McpServerController.ServerPort;
|
||||
int savedPort = McpEditorSettings.GetCustomPort();
|
||||
|
||||
if (savedPort != actualPort)
|
||||
{
|
||||
McpEditorSettings.SetCustomPort(actualPort);
|
||||
}
|
||||
}
|
||||
|
||||
private ServerStatusData CreateServerStatusData()
|
||||
{
|
||||
(bool isRunning, int port, bool _) = McpServerController.GetServerStatus();
|
||||
string status = isRunning ? "Running" : "Stopped";
|
||||
Color statusColor = isRunning ? Color.green : Color.red;
|
||||
|
||||
return new ServerStatusData(isRunning, port, status, statusColor);
|
||||
}
|
||||
|
||||
private ServerControlsData CreateServerControlsData()
|
||||
{
|
||||
bool isRunning = McpServerController.IsServerRunning;
|
||||
int customPort = McpEditorSettings.GetCustomPort();
|
||||
|
||||
bool hasPortWarning = false;
|
||||
string portWarningMessage = null;
|
||||
|
||||
if (!isRunning)
|
||||
{
|
||||
if (!McpPortValidator.ValidatePort(customPort))
|
||||
{
|
||||
hasPortWarning = true;
|
||||
portWarningMessage = $"Port {customPort} is invalid. Port must be 1024 or higher and not a reserved system port.";
|
||||
}
|
||||
else if (NetworkUtility.IsPortInUse(customPort))
|
||||
{
|
||||
hasPortWarning = true;
|
||||
portWarningMessage = $"Port {customPort} is already in use. Please choose a different port or stop the other process using this port.";
|
||||
}
|
||||
}
|
||||
|
||||
return new ServerControlsData(customPort, isRunning, !isRunning, hasPortWarning, portWarningMessage);
|
||||
}
|
||||
|
||||
private void ToggleServer()
|
||||
{
|
||||
if (McpServerController.IsServerRunning)
|
||||
{
|
||||
McpServerController.StopServer();
|
||||
}
|
||||
else
|
||||
{
|
||||
StartServer();
|
||||
}
|
||||
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void StartServer()
|
||||
{
|
||||
int port = McpEditorSettings.GetCustomPort();
|
||||
|
||||
if (!McpPortValidator.ValidatePort(port))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Port Error",
|
||||
$"Port must be between {McpServerConfig.MIN_PORT_NUMBER} and {McpServerConfig.MAX_PORT_NUMBER}",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
// Config auto-update is best-effort; do not block server startup on unrelated config-file problems
|
||||
try
|
||||
{
|
||||
McpConfigAutoUpdater.UpdateAllConfiguredEditors(port);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[uLoopMCP] Failed to auto-update configurations: {ex.Message}");
|
||||
}
|
||||
|
||||
McpServerController.StartServer(port);
|
||||
}
|
||||
|
||||
private void OnPortChanged(int port)
|
||||
{
|
||||
McpEditorSettings.SetCustomPort(port);
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void OnServerStateChanged()
|
||||
{
|
||||
_needsRepaint = true;
|
||||
}
|
||||
|
||||
private void OnEditorUpdate()
|
||||
{
|
||||
if (!_needsRepaint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_needsRepaint = false;
|
||||
RefreshUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48d305aead26c4120aca9cef9d4fcce7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,157 @@
|
||||
/* ===========================================
|
||||
ServerEditorWindow Stylesheet
|
||||
Extracted from McpEditorWindow.uss for the
|
||||
standalone Server window.
|
||||
=========================================== */
|
||||
|
||||
:root {
|
||||
--mcp-color-success: #4caf50;
|
||||
--mcp-color-danger: #ef5350;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Section
|
||||
=========================================== */
|
||||
.mcp-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-section--box {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Server Status
|
||||
=========================================== */
|
||||
.mcp-server-status {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator--running {
|
||||
background-color: var(--mcp-color-success);
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator--stopped {
|
||||
background-color: var(--mcp-color-danger);
|
||||
}
|
||||
|
||||
.mcp-server-status__text {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.mcp-server-status__text--running {
|
||||
color: var(--mcp-color-success);
|
||||
border-color: var(--mcp-color-success);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.mcp-server-status__text--stopped {
|
||||
color: var(--mcp-color-danger);
|
||||
border-color: var(--mcp-color-danger);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.mcp-server-status__spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-server-status__port-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-server-status__port-field {
|
||||
width: 60px;
|
||||
-unity-text-align: middle-right;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Server Controls
|
||||
=========================================== */
|
||||
.mcp-server-controls {
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.mcp-server-controls .mcp-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Warning Container
|
||||
=========================================== */
|
||||
.mcp-warning-container {
|
||||
display: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-warning-container--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-warning-label {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Button
|
||||
=========================================== */
|
||||
.mcp-button {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-button--compact {
|
||||
height: 22px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mcp-button--start {
|
||||
background-color: #2e7d32;
|
||||
border-color: var(--mcp-color-success);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mcp-button--start:hover {
|
||||
background-color: #388e3c;
|
||||
}
|
||||
|
||||
.mcp-button--stop {
|
||||
background-color: #4a4a4a;
|
||||
border-color: #666666;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.mcp-button--stop:hover {
|
||||
background-color: #555555;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eda86d2b6ce0b4f4298f640ca17c9539
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
<ui:VisualElement name="server-section" class="mcp-section mcp-section--box">
|
||||
<ui:VisualElement class="mcp-server-status">
|
||||
<ui:VisualElement name="server-status-indicator" class="mcp-server-status__indicator" />
|
||||
<ui:Label name="status-value" text="Stopped" class="mcp-server-status__text mcp-server-status__text--stopped" />
|
||||
<ui:VisualElement class="mcp-server-status__spacer" />
|
||||
<ui:Label text="Port:" class="mcp-server-status__port-label" />
|
||||
<uie:IntegerField name="port-field" value="8080" class="mcp-server-status__port-field" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="port-warning-container" class="mcp-warning-container">
|
||||
<ui:Label name="port-warning-label" class="mcp-warning-label" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="mcp-server-controls">
|
||||
<ui:Button name="toggle-server-button" text="Start Server" class="mcp-button mcp-button--compact mcp-button--start" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92fdf4f625a914ca186abb4fb130a4c7
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4ee9a3380d8747d8ad13bffd699d109
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,140 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4db180d6e51f04c5781b7952488e894b
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecd19e180f01a401a8249da9919bc8f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,315 @@
|
||||
/* ===========================================
|
||||
SetupWizardWindow Stylesheet
|
||||
=========================================== */
|
||||
|
||||
:root {
|
||||
--setup-color-success: #4caf50;
|
||||
--setup-color-warning: #ffd200;
|
||||
--setup-color-danger: #ef5350;
|
||||
--setup-color-muted: #999999;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Main Container
|
||||
=========================================== */
|
||||
.setup-main-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Prerequisite
|
||||
=========================================== */
|
||||
.setup-prerequisite {
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.setup-prerequisite--warning {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
}
|
||||
|
||||
.setup-prerequisite--ok {
|
||||
background-color: #1b3a1b;
|
||||
border-color: #2e7d32;
|
||||
}
|
||||
|
||||
.setup-prerequisite--compact {
|
||||
justify-content: center;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.setup-prerequisite__title {
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.setup-prerequisite--compact .setup-prerequisite__title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setup-prerequisite--warning .setup-prerequisite__title {
|
||||
color: var(--setup-color-warning);
|
||||
}
|
||||
|
||||
.setup-prerequisite--ok .setup-prerequisite__title {
|
||||
color: var(--setup-color-success);
|
||||
}
|
||||
|
||||
.setup-prerequisite__message {
|
||||
white-space: normal;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Step
|
||||
=========================================== */
|
||||
.setup-step {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.setup-step__title {
|
||||
-unity-font-style: bold;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.setup-step__content {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.setup-step__status-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.setup-step__status-label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Status Icon
|
||||
=========================================== */
|
||||
.setup-status-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.setup-status-icon--pending {
|
||||
background-color: var(--setup-color-muted);
|
||||
}
|
||||
|
||||
.setup-status-icon--success {
|
||||
background-color: var(--setup-color-success);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Target
|
||||
=========================================== */
|
||||
.setup-target-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setup-target-row__label {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.setup-target-row__field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setup-target-list {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.setup-toggle-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.setup-toggle-row__toggle {
|
||||
width: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.setup-toggle-row__label {
|
||||
flex-grow: 1;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.setup-target-item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.setup-target-item__label {
|
||||
flex-grow: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.setup-target-item__status {
|
||||
flex-shrink: 0;
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.setup-target-item__status--installed {
|
||||
color: var(--setup-color-success);
|
||||
}
|
||||
|
||||
.setup-target-item__status--checking {
|
||||
color: var(--setup-color-muted);
|
||||
}
|
||||
|
||||
.setup-target-item__status--outdated {
|
||||
color: var(--setup-color-warning);
|
||||
}
|
||||
|
||||
.setup-target-item__status--missing {
|
||||
color: var(--setup-color-muted);
|
||||
}
|
||||
|
||||
.setup-target-item__status--different-layout {
|
||||
color: var(--setup-color-warning);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Button
|
||||
=========================================== */
|
||||
.setup-button {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.setup-button--small {
|
||||
height: 22px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
font-size: 11px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.setup-button--primary {
|
||||
background-color: #2e7d32;
|
||||
border-color: var(--setup-color-success);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.setup-button--primary:hover {
|
||||
background-color: #388e3c;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Element: Hint Label
|
||||
=========================================== */
|
||||
.setup-step__hint-label {
|
||||
font-size: 11px;
|
||||
color: var(--setup-color-muted);
|
||||
white-space: normal;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.setup-step__divider {
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgba(255, 255, 255, 0.12);
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Footer
|
||||
=========================================== */
|
||||
.setup-footer__toggle {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.setup-footer__hint-label {
|
||||
font-size: 11px;
|
||||
color: var(--setup-color-muted);
|
||||
white-space: normal;
|
||||
margin-top: 2px;
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
.setup-footer__button-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.setup-footer__button-row .setup-button {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.setup-footer__github-link {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgba(255, 255, 255, 0.12);
|
||||
padding-top: 8px;
|
||||
margin-top: 10px;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.setup-footer__github-link-label {
|
||||
font-size: 11px;
|
||||
color: #c3cad3;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.setup-footer__github-link-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.78;
|
||||
-unity-background-scale-mode: scale-to-fit;
|
||||
}
|
||||
|
||||
.setup-footer__github-link--hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.setup-footer__github-link-label--hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.setup-footer__github-link-icon--hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.setup-step__button-row {
|
||||
flex-direction: row;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.setup-step__button-row .setup-button {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Utility: Hidden
|
||||
=========================================== */
|
||||
.setup-hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b1f46c357b5f47da8d2b87979a0664e
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
<ui:ScrollView class="setup-main-container">
|
||||
<!-- Prerequisite: Node.js check -->
|
||||
<ui:VisualElement name="nodejs-warning" class="setup-prerequisite setup-prerequisite--warning">
|
||||
<ui:Label text="⚠ Node.js is required" class="setup-prerequisite__title" />
|
||||
<ui:Label text="Please install Node.js to continue." class="setup-prerequisite__message" />
|
||||
<ui:Button name="refresh-button" text="Refresh" class="setup-button setup-button--small" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="nodejs-ok" class="setup-prerequisite setup-prerequisite--ok setup-prerequisite--compact setup-hidden">
|
||||
<ui:Label text="✓ Node.js detected" class="setup-prerequisite__title" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="project-cli-version-row" class="setup-toggle-row">
|
||||
<ui:Toggle name="project-cli-version-toggle" class="setup-toggle-row__toggle" />
|
||||
<ui:Label
|
||||
name="project-cli-version-label"
|
||||
text="Use project CLI version"
|
||||
tooltip="Run installed skills through the Unity package version of uloop-cli."
|
||||
class="setup-toggle-row__label" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Global CLI -->
|
||||
<ui:VisualElement name="step-1" class="setup-step">
|
||||
<ui:Label text="Global CLI" class="setup-step__title" />
|
||||
<ui:VisualElement class="setup-step__content">
|
||||
<ui:VisualElement class="setup-step__status-row">
|
||||
<ui:VisualElement name="cli-status-icon" class="setup-status-icon" />
|
||||
<ui:Label name="cli-status-label" text="Checking..." class="setup-step__status-label" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="install-cli-button" text="Install CLI" class="setup-button" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Skills -->
|
||||
<ui:VisualElement name="step-2" class="setup-step">
|
||||
<ui:Label text="Skills" class="setup-step__title" />
|
||||
<ui:VisualElement class="setup-step__content">
|
||||
<ui:VisualElement name="group-skills-row" class="setup-toggle-row">
|
||||
<ui:Toggle name="group-skills-toggle" class="setup-toggle-row__toggle" />
|
||||
<ui:Label
|
||||
name="group-skills-label"
|
||||
text="Group in subfolder"
|
||||
class="setup-toggle-row__label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="skills-target-row" class="setup-target-row">
|
||||
<ui:Label text="Target:" class="setup-target-row__label" />
|
||||
<uie:EnumField name="skills-target-field" class="setup-target-row__field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="skills-target-list" class="setup-target-list" />
|
||||
<ui:VisualElement name="skills-status-divider" class="setup-step__divider" />
|
||||
<ui:Label name="skills-status-label" text="" class="setup-step__status-label" />
|
||||
<ui:VisualElement class="setup-step__button-row">
|
||||
<ui:Button name="install-skills-button" text="Install Skills" class="setup-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="skills-hint-label" text="Install skills later from Window > Unity CLI Loop > Settings." class="setup-step__hint-label" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Footer -->
|
||||
<ui:VisualElement class="setup-footer">
|
||||
<ui:VisualElement class="setup-footer__button-row">
|
||||
<ui:Button name="open-settings-button" text="Open Settings" class="setup-button setup-button--primary" />
|
||||
<ui:Button name="close-button" text="Close" class="setup-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Toggle
|
||||
name="suppress-auto-show-toggle"
|
||||
text="Don't show this after updates"
|
||||
class="setup-footer__toggle" />
|
||||
<ui:Label
|
||||
text="Reopen this from Window > Unity CLI Loop > Setup Wizard."
|
||||
class="setup-footer__hint-label" />
|
||||
<ui:VisualElement name="github-link-row" class="setup-footer__github-link">
|
||||
<ui:Label name="github-link-label" text="Star on GitHub" class="setup-footer__github-link-label" />
|
||||
<ui:Image name="github-link-icon" class="setup-footer__github-link-icon" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:ScrollView>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64588000e656f4a05be63d044cd1ad74
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
using System;
|
||||
|
||||
public readonly struct SkillsTargetSelection
|
||||
{
|
||||
public readonly string DisplayName;
|
||||
public readonly string DirectoryName;
|
||||
public readonly string InstallFlag;
|
||||
public readonly string InstallArguments;
|
||||
|
||||
public SkillsTargetSelection(
|
||||
string displayName,
|
||||
string directoryName,
|
||||
string installFlag,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
DirectoryName = directoryName;
|
||||
InstallFlag = installFlag;
|
||||
InstallArguments = groupSkillsUnderUnityCliLoop
|
||||
? $"skills install {installFlag}"
|
||||
: $"skills install {installFlag} --flat";
|
||||
}
|
||||
}
|
||||
|
||||
public static class SkillsTargetSelectionResolver
|
||||
{
|
||||
public static SkillsTargetSelection Resolve(
|
||||
SkillsTarget target,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
return target switch
|
||||
{
|
||||
SkillsTarget.Claude => new("Claude Code", ".claude", "--claude", groupSkillsUnderUnityCliLoop),
|
||||
SkillsTarget.Cursor => new("Cursor", ".cursor", "--cursor", groupSkillsUnderUnityCliLoop),
|
||||
SkillsTarget.Gemini => new("Gemini CLI", ".gemini", "--gemini", groupSkillsUnderUnityCliLoop),
|
||||
SkillsTarget.Codex => new("Codex CLI", ".codex", "--codex", groupSkillsUnderUnityCliLoop),
|
||||
SkillsTarget.Agents => new("Other (.agents)", ".agents", "--agents", groupSkillsUnderUnityCliLoop),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null)
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsInstalled(CliSetupData data, SkillsTarget target)
|
||||
{
|
||||
return target switch
|
||||
{
|
||||
SkillsTarget.Claude => data.IsClaudeSkillsInstalled,
|
||||
SkillsTarget.Cursor => data.IsCursorSkillsInstalled,
|
||||
SkillsTarget.Gemini => data.IsGeminiSkillsInstalled,
|
||||
SkillsTarget.Codex => data.IsCodexSkillsInstalled,
|
||||
SkillsTarget.Agents => data.IsAgentsSkillsInstalled,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static SkillInstallState GetInstallState(CliSetupData data, SkillsTarget target)
|
||||
{
|
||||
return data.SelectedTarget == target ? data.SelectedTargetInstallState : SkillInstallState.Missing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdcc9ecf5f9ec4e9684b2e1d0ed30976
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b85f09cd2bbf8472386a21e37979f9ea
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57740db4675d74afb92c26bcc9d11913
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility for binding UI Toolkit elements to data without triggering change events.
|
||||
/// SetValueWithoutNotify prevents infinite loops when updating UI from model changes.
|
||||
/// </summary>
|
||||
public static class ViewDataBinder
|
||||
{
|
||||
public static void BindToggle(Toggle toggle, Func<bool> getter, Action<bool> onChanged)
|
||||
{
|
||||
toggle.SetValueWithoutNotify(getter());
|
||||
toggle.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
|
||||
}
|
||||
|
||||
public static void BindIntegerField(IntegerField field, Func<int> getter, Action<int> onChanged)
|
||||
{
|
||||
field.SetValueWithoutNotify(getter());
|
||||
field.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
|
||||
}
|
||||
|
||||
public static void BindEnumField<T>(EnumField field, Func<T> getter, Action<T> onChanged) where T : Enum
|
||||
{
|
||||
field.Init(getter());
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue is T newValue)
|
||||
{
|
||||
onChanged(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void BindFoldout(Foldout foldout, Func<bool> getter, Action<bool> onChanged)
|
||||
{
|
||||
foldout.SetValueWithoutNotify(getter());
|
||||
foldout.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
|
||||
}
|
||||
|
||||
public static void BindButton(Button button, Action onClick)
|
||||
{
|
||||
button.clicked += onClick;
|
||||
}
|
||||
|
||||
public static void BindLabel(Label label, Action<Label> onClick)
|
||||
{
|
||||
label.RegisterCallback<ClickEvent>(evt => onClick(label));
|
||||
}
|
||||
|
||||
public static void UpdateToggle(Toggle toggle, bool value)
|
||||
{
|
||||
toggle.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void UpdateIntegerField(IntegerField field, int value)
|
||||
{
|
||||
field.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void UpdateEnumField<T>(EnumField field, T value) where T : Enum
|
||||
{
|
||||
field.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void UpdateFoldout(Foldout foldout, bool value)
|
||||
{
|
||||
foldout.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void SetVisible(VisualElement element, bool visible)
|
||||
{
|
||||
element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
public static void SetEnabled(VisualElement element, bool enabled)
|
||||
{
|
||||
element.SetEnabled(enabled);
|
||||
}
|
||||
|
||||
public static void AddModifierClass(VisualElement element, string baseClass, string modifier, bool condition)
|
||||
{
|
||||
string modifierClass = $"{baseClass}--{modifier}";
|
||||
if (condition)
|
||||
{
|
||||
element.AddToClassList(modifierClass);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.RemoveFromClassList(modifierClass);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ToggleClass(VisualElement element, string className, bool condition)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
element.AddToClassList(className);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.RemoveFromClassList(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0262ce83ca8f45d89b43d1dbd0ed551
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfe94e1e1bb544deeb95197f97166dfc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,333 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class CliSetupSection
|
||||
{
|
||||
private readonly VisualElement _cliStatusIcon;
|
||||
private readonly VisualElement _globalCliStatusRow;
|
||||
private readonly Label _cliStatusLabel;
|
||||
private readonly Button _refreshCliVersionButton;
|
||||
private readonly Button _installCliButton;
|
||||
private readonly VisualElement _globalCliSeparator;
|
||||
private readonly EnumField _skillsTargetField;
|
||||
private readonly Button _refreshSkillsStateButton;
|
||||
private readonly VisualElement _groupSkillsRow;
|
||||
private readonly Toggle _groupSkillsToggle;
|
||||
private readonly Label _groupSkillsLabel;
|
||||
private readonly VisualElement _projectCliVersionRow;
|
||||
private readonly Toggle _projectCliVersionToggle;
|
||||
private readonly Button _installSkillsButton;
|
||||
private readonly VisualElement _skillsSubsection;
|
||||
|
||||
private CliSetupData _lastData;
|
||||
private bool _isTargetFieldInitialized;
|
||||
|
||||
public event Action OnRefreshCliVersion;
|
||||
public event Action OnInstallCli;
|
||||
public event Action OnInstallSkills;
|
||||
public event Action OnRefreshSkillsState;
|
||||
public event Action<SkillsTarget> OnSkillsTargetChanged;
|
||||
public event Action<bool> OnGroupSkillsChanged;
|
||||
public event Action<bool> OnUseProjectCliVersionChanged;
|
||||
|
||||
public CliSetupSection(VisualElement root)
|
||||
{
|
||||
_cliStatusIcon = root.Q<VisualElement>("cli-status-icon");
|
||||
_globalCliStatusRow = root.Q<VisualElement>("global-cli-status-row");
|
||||
_cliStatusLabel = root.Q<Label>("cli-status-label");
|
||||
_refreshCliVersionButton = root.Q<Button>("refresh-cli-version-button");
|
||||
_installCliButton = root.Q<Button>("install-cli-button");
|
||||
_globalCliSeparator = root.Q<VisualElement>("global-cli-separator");
|
||||
_skillsTargetField = root.Q<EnumField>("skills-target-field");
|
||||
_refreshSkillsStateButton = root.Q<Button>("refresh-skills-state-button");
|
||||
_groupSkillsRow = root.Q<VisualElement>("group-skills-row");
|
||||
_groupSkillsToggle = root.Q<Toggle>("group-skills-toggle");
|
||||
_groupSkillsLabel = root.Q<Label>("group-skills-label");
|
||||
_projectCliVersionRow = root.Q<VisualElement>("project-cli-version-row");
|
||||
_projectCliVersionToggle = root.Q<Toggle>("project-cli-version-toggle");
|
||||
_installSkillsButton = root.Q<Button>("install-skills-button");
|
||||
_skillsSubsection = root.Q<VisualElement>("skills-subsection");
|
||||
}
|
||||
|
||||
public void SetupBindings()
|
||||
{
|
||||
_refreshCliVersionButton.clicked += () => OnRefreshCliVersion?.Invoke();
|
||||
_installCliButton.clicked += () => OnInstallCli?.Invoke();
|
||||
_installSkillsButton.clicked += () => OnInstallSkills?.Invoke();
|
||||
_refreshSkillsStateButton.clicked += () => OnRefreshSkillsState?.Invoke();
|
||||
_groupSkillsToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
OnGroupSkillsChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
_groupSkillsRow.RegisterCallback<ClickEvent>(HandleGroupSkillsRowClicked);
|
||||
_projectCliVersionToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
OnUseProjectCliVersionChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
_projectCliVersionRow.RegisterCallback<ClickEvent>(HandleProjectCliVersionRowClicked);
|
||||
}
|
||||
|
||||
public void Update(CliSetupData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
UpdateCliStatus(data);
|
||||
UpdateGlobalCliVisibility(data);
|
||||
UpdateRefreshButton(data);
|
||||
UpdateInstallCliButton(data);
|
||||
InitializeTargetFieldIfNeeded(data);
|
||||
UpdateRefreshSkillsButton(data);
|
||||
UpdateGroupSkillsToggle(data);
|
||||
UpdateProjectCliVersionToggle(data);
|
||||
UpdateSkillsSubsection(data);
|
||||
UpdateInstallSkillsButton(data);
|
||||
}
|
||||
|
||||
private void UpdateCliStatus(CliSetupData data)
|
||||
{
|
||||
if (data.IsChecking)
|
||||
{
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--installed", false);
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--not-installed", false);
|
||||
_cliStatusLabel.text = "uloop-cli: Checking...";
|
||||
return;
|
||||
}
|
||||
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--installed", data.IsCliInstalled);
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--not-installed", !data.IsCliInstalled);
|
||||
|
||||
if (data.IsCliInstalled && data.CliVersion != null)
|
||||
{
|
||||
_cliStatusLabel.text = $"uloop-cli: v{data.CliVersion}";
|
||||
return;
|
||||
}
|
||||
|
||||
_cliStatusLabel.text = "uloop-cli: Not installed";
|
||||
}
|
||||
|
||||
private void UpdateRefreshButton(CliSetupData data)
|
||||
{
|
||||
_refreshCliVersionButton.SetEnabled(!data.IsChecking);
|
||||
}
|
||||
|
||||
private void UpdateGlobalCliVisibility(CliSetupData data)
|
||||
{
|
||||
bool visible = !data.UseProjectCliVersion;
|
||||
ViewDataBinder.SetVisible(_globalCliStatusRow, visible);
|
||||
ViewDataBinder.SetVisible(_installCliButton, visible);
|
||||
ViewDataBinder.SetVisible(_globalCliSeparator, visible);
|
||||
}
|
||||
|
||||
private void UpdateInstallCliButton(CliSetupData data)
|
||||
{
|
||||
if (data.IsChecking)
|
||||
{
|
||||
SetCliButton("Checking...", false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.IsInstallingCli)
|
||||
{
|
||||
SetCliButton("Installing...", false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.IsCliInstalled)
|
||||
{
|
||||
SetCliButton("Install CLI", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.NeedsUpdate)
|
||||
{
|
||||
SetCliButton($"Update CLI (v{data.CliVersion} \u2192 v{data.PackageVersion})", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.NeedsDowngrade)
|
||||
{
|
||||
SetCliButton($"Downgrade CLI (v{data.CliVersion} \u2192 v{data.PackageVersion})", true);
|
||||
return;
|
||||
}
|
||||
|
||||
SetCliButton("Up to date", false);
|
||||
}
|
||||
|
||||
private void SetCliButton(string text, bool enabled)
|
||||
{
|
||||
_installCliButton.text = text;
|
||||
_installCliButton.SetEnabled(enabled);
|
||||
ViewDataBinder.ToggleClass(_installCliButton, "mcp-button--disabled", !enabled);
|
||||
}
|
||||
|
||||
private void InitializeTargetFieldIfNeeded(CliSetupData data)
|
||||
{
|
||||
if (!_isTargetFieldInitialized)
|
||||
{
|
||||
_skillsTargetField.Init(data.SelectedTarget);
|
||||
_skillsTargetField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue is SkillsTarget newValue)
|
||||
{
|
||||
OnSkillsTargetChanged?.Invoke(newValue);
|
||||
}
|
||||
});
|
||||
_isTargetFieldInitialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewDataBinder.UpdateEnumField(_skillsTargetField, data.SelectedTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRefreshSkillsButton(CliSetupData data)
|
||||
{
|
||||
bool canManageSkills = CanManageSkills(data);
|
||||
bool enabled = canManageSkills
|
||||
&& !data.IsChecking
|
||||
&& !data.IsInstallingCli
|
||||
&& !data.IsInstallingSkills;
|
||||
_refreshSkillsStateButton.SetEnabled(enabled);
|
||||
ViewDataBinder.ToggleClass(_refreshSkillsStateButton, "mcp-button--disabled", !enabled);
|
||||
}
|
||||
|
||||
private void UpdateGroupSkillsToggle(CliSetupData data)
|
||||
{
|
||||
ViewDataBinder.SetVisible(_groupSkillsRow, false);
|
||||
ViewDataBinder.UpdateToggle(_groupSkillsToggle, data.GroupSkillsUnderUnityCliLoop);
|
||||
_groupSkillsToggle.SetEnabled(CanManageSkills(data)
|
||||
&& !data.IsChecking
|
||||
&& !data.IsInstallingCli
|
||||
&& !data.IsInstallingSkills);
|
||||
}
|
||||
|
||||
private void UpdateProjectCliVersionToggle(CliSetupData data)
|
||||
{
|
||||
ViewDataBinder.UpdateToggle(_projectCliVersionToggle, data.UseProjectCliVersion);
|
||||
_projectCliVersionToggle.SetEnabled(!data.IsChecking
|
||||
&& !data.IsInstallingCli
|
||||
&& !data.IsInstallingSkills);
|
||||
}
|
||||
|
||||
private void UpdateSkillsSubsection(CliSetupData data)
|
||||
{
|
||||
bool enabled = CanManageSkills(data) && !data.IsChecking && !data.IsInstallingCli;
|
||||
_skillsSubsection.SetEnabled(enabled);
|
||||
}
|
||||
|
||||
private void UpdateInstallSkillsButton(CliSetupData data)
|
||||
{
|
||||
string label = GetInstallSkillsButtonText(
|
||||
CanManageSkills(data),
|
||||
data.IsInstallingSkills,
|
||||
data.SelectedTargetInstallState);
|
||||
bool enabled = IsInstallSkillsButtonEnabled(
|
||||
CanManageSkills(data),
|
||||
data.IsInstallingSkills,
|
||||
data.IsChecking || data.IsInstallingCli,
|
||||
data.SelectedTargetInstallState);
|
||||
SetSkillsButton(label, enabled);
|
||||
}
|
||||
|
||||
private static bool CanManageSkills(CliSetupData data)
|
||||
{
|
||||
return data.IsCliInstalled || data.UseProjectCliVersion;
|
||||
}
|
||||
|
||||
private void SetSkillsButton(string text, bool enabled)
|
||||
{
|
||||
_installSkillsButton.text = text;
|
||||
_installSkillsButton.SetEnabled(enabled);
|
||||
ViewDataBinder.ToggleClass(_installSkillsButton, "mcp-button--disabled", !enabled);
|
||||
}
|
||||
|
||||
internal static string GetInstallSkillsButtonText(
|
||||
bool isCliInstalled,
|
||||
bool isInstallingSkills,
|
||||
SkillInstallState installState)
|
||||
{
|
||||
if (isInstallingSkills)
|
||||
{
|
||||
return "Installing...";
|
||||
}
|
||||
|
||||
if (!isCliInstalled)
|
||||
{
|
||||
return "Install Skills";
|
||||
}
|
||||
|
||||
return installState switch
|
||||
{
|
||||
SkillInstallState.Checking => "Checking...",
|
||||
SkillInstallState.Installed => "Installed",
|
||||
SkillInstallState.Outdated => "Update Skills",
|
||||
_ => "Install Skills"
|
||||
};
|
||||
}
|
||||
|
||||
internal static bool IsInstallSkillsButtonEnabled(
|
||||
bool isCliInstalled,
|
||||
bool isInstallingSkills,
|
||||
bool isChecking,
|
||||
SkillInstallState installState)
|
||||
{
|
||||
if (!isCliInstalled || isInstallingSkills || isChecking)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return installState switch
|
||||
{
|
||||
SkillInstallState.Checking => false,
|
||||
SkillInstallState.Installed => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private void HandleGroupSkillsRowClicked(ClickEvent evt)
|
||||
{
|
||||
evt.StopPropagation();
|
||||
if (!_groupSkillsToggle.enabledSelf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.target is VisualElement targetElement && _groupSkillsToggle.Contains(targetElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool newValue = !_groupSkillsToggle.value;
|
||||
_groupSkillsToggle.SetValueWithoutNotify(newValue);
|
||||
OnGroupSkillsChanged?.Invoke(newValue);
|
||||
}
|
||||
|
||||
private void HandleProjectCliVersionRowClicked(ClickEvent evt)
|
||||
{
|
||||
evt.StopPropagation();
|
||||
if (!_projectCliVersionToggle.enabledSelf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.target is VisualElement targetElement && _projectCliVersionToggle.Contains(targetElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool newValue = !_projectCliVersionToggle.value;
|
||||
_projectCliVersionToggle.SetValueWithoutNotify(newValue);
|
||||
OnUseProjectCliVersionChanged?.Invoke(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6448f8d9a121c4da2ac15cf003171b63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class ConnectedToolsSection
|
||||
{
|
||||
private readonly VisualElement _sectionContainer;
|
||||
private readonly Foldout _foldout;
|
||||
private readonly VisualElement _clientsList;
|
||||
|
||||
private bool _hasLastValidData;
|
||||
private ConnectedToolsData _lastData;
|
||||
|
||||
public event Action<bool> OnFoldoutChanged;
|
||||
|
||||
public ConnectedToolsSection(VisualElement root)
|
||||
{
|
||||
_sectionContainer = root.Q<VisualElement>("connected-tools-section");
|
||||
_foldout = root.Q<Foldout>("connected-tools-foldout");
|
||||
_clientsList = root.Q<VisualElement>("clients-list");
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_foldout.RegisterValueChangedCallback(evt => OnFoldoutChanged?.Invoke(evt.newValue));
|
||||
}
|
||||
|
||||
public void Update(ConnectedToolsData data)
|
||||
{
|
||||
ViewDataBinder.SetVisible(_sectionContainer, data.ShowSection);
|
||||
|
||||
if (!data.ShowSection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.ShowReconnectingUI && _hasLastValidData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewDataBinder.UpdateFoldout(_foldout, data.ShowFoldout);
|
||||
UpdateClientsList(data);
|
||||
_lastData = data;
|
||||
}
|
||||
|
||||
private void UpdateClientsList(ConnectedToolsData data)
|
||||
{
|
||||
_clientsList.Clear();
|
||||
|
||||
if (data.Clients == null || data.Clients.Count == 0)
|
||||
{
|
||||
_hasLastValidData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
System.Collections.Generic.List<ConnectedClient> validClients = data.Clients
|
||||
.Where(client => IsValidClientName(client.ClientName))
|
||||
.ToList();
|
||||
|
||||
foreach (ConnectedClient client in validClients)
|
||||
{
|
||||
VisualElement clientItem = CreateClientItem(client);
|
||||
_clientsList.Add(clientItem);
|
||||
}
|
||||
|
||||
_hasLastValidData = validClients.Count > 0;
|
||||
}
|
||||
|
||||
private VisualElement CreateClientItem(ConnectedClient client)
|
||||
{
|
||||
VisualElement container = new VisualElement();
|
||||
container.AddToClassList("mcp-client");
|
||||
|
||||
Label iconLabel = new Label(McpUIConstants.CLIENT_ICON);
|
||||
iconLabel.AddToClassList("mcp-client__icon");
|
||||
container.Add(iconLabel);
|
||||
|
||||
Label nameLabel = new Label(client.ClientName);
|
||||
nameLabel.AddToClassList("mcp-client__name");
|
||||
container.Add(nameLabel);
|
||||
|
||||
Label portLabel = new Label($":{client.Port}");
|
||||
portLabel.AddToClassList("mcp-client__port");
|
||||
container.Add(portLabel);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private bool IsValidClientName(string clientName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(clientName) && clientName != McpConstants.UNKNOWN_CLIENT_NAME;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f622365cf49674deb8b852dede89f11d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class ConnectionModeSection
|
||||
{
|
||||
private readonly Foldout _foldout;
|
||||
private readonly Button _mcpTabButton;
|
||||
private readonly Button _cliTabButton;
|
||||
private ConnectionModeData _lastData;
|
||||
|
||||
public event Action<ConnectionMode> OnModeChanged;
|
||||
public event Action<bool> OnFoldoutChanged;
|
||||
|
||||
public ConnectionModeSection(VisualElement root)
|
||||
{
|
||||
_foldout = root.Q<Foldout>("configuration-foldout");
|
||||
_mcpTabButton = root.Q<Button>("mcp-tab-button");
|
||||
_cliTabButton = root.Q<Button>("cli-tab-button");
|
||||
|
||||
_mcpTabButton.clicked += () => OnModeChanged?.Invoke(ConnectionMode.MCP);
|
||||
_cliTabButton.clicked += () => OnModeChanged?.Invoke(ConnectionMode.CLI);
|
||||
_foldout.RegisterValueChangedCallback(evt => OnFoldoutChanged?.Invoke(evt.newValue));
|
||||
}
|
||||
|
||||
public void Update(ConnectionModeData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
bool isMcp = data.Mode == ConnectionMode.MCP;
|
||||
|
||||
ViewDataBinder.ToggleClass(_mcpTabButton, "mcp-tab-button--active", isMcp);
|
||||
ViewDataBinder.ToggleClass(_cliTabButton, "mcp-tab-button--active", !isMcp);
|
||||
}
|
||||
|
||||
public void UpdateFoldout(bool show)
|
||||
{
|
||||
ViewDataBinder.UpdateFoldout(_foldout, show);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 927b0a3b6f73f4ef79fb88ad7228f9c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section for LLM tool configuration (Cursor, Claude Code, etc).
|
||||
/// Handles editor type selection and MCP settings file generation.
|
||||
/// </summary>
|
||||
public class EditorConfigSection
|
||||
{
|
||||
private readonly EnumField _editorTypeField;
|
||||
private readonly VisualElement _repositoryRootRow;
|
||||
private readonly Toggle _repositoryRootToggle;
|
||||
private readonly Label _configErrorLabel;
|
||||
private readonly Button _configureButton;
|
||||
private readonly Button _deleteConfigButton;
|
||||
private readonly Button _openSettingsButton;
|
||||
|
||||
private EditorConfigData _lastData;
|
||||
private bool _isInitialized;
|
||||
|
||||
public event Action<McpEditorType> OnEditorTypeChanged;
|
||||
public event Action<bool> OnRepositoryRootChanged;
|
||||
public event Action OnConfigureClicked;
|
||||
public event Action OnDeleteConfigClicked;
|
||||
public event Action OnOpenSettingsClicked;
|
||||
|
||||
public EditorConfigSection(VisualElement root)
|
||||
{
|
||||
_editorTypeField = root.Q<EnumField>("editor-type-field");
|
||||
_repositoryRootRow = root.Q<VisualElement>("repository-root-row");
|
||||
_repositoryRootToggle = root.Q<Toggle>("repository-root-toggle");
|
||||
_configErrorLabel = root.Q<Label>("config-error-label");
|
||||
_configureButton = root.Q<Button>("configure-button");
|
||||
_deleteConfigButton = root.Q<Button>("delete-config-button");
|
||||
_openSettingsButton = root.Q<Button>("open-settings-button");
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_repositoryRootToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
// Foldout uses an internal Toggle that listens for ChangeEvent<bool>.
|
||||
// Without StopPropagation, this event bubbles up and collapses the Foldout.
|
||||
evt.StopPropagation();
|
||||
OnRepositoryRootChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
_configureButton.clicked += () => OnConfigureClicked?.Invoke();
|
||||
_deleteConfigButton.clicked += () => OnDeleteConfigClicked?.Invoke();
|
||||
_openSettingsButton.clicked += () => OnOpenSettingsClicked?.Invoke();
|
||||
}
|
||||
|
||||
public void Update(EditorConfigData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
InitializeEnumFieldIfNeeded(data);
|
||||
|
||||
UpdateRepositoryRootRow(data);
|
||||
UpdateConfigError(data);
|
||||
UpdateConfigureButton(data);
|
||||
UpdateDeleteButton(data);
|
||||
UpdateOpenSettingsButton(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EnumField.Init() can only be called once; subsequent calls reset the field and cause visual glitches.
|
||||
/// </summary>
|
||||
private void InitializeEnumFieldIfNeeded(EditorConfigData data)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_editorTypeField.Init(data.SelectedEditor);
|
||||
_editorTypeField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue is McpEditorType newValue)
|
||||
{
|
||||
OnEditorTypeChanged?.Invoke(newValue);
|
||||
}
|
||||
});
|
||||
_isInitialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewDataBinder.UpdateEnumField(_editorTypeField, data.SelectedEditor);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRepositoryRootRow(EditorConfigData data)
|
||||
{
|
||||
bool showRow = data.SupportsRepositoryRootToggle && data.ShowRepositoryRootToggle;
|
||||
ViewDataBinder.SetVisible(_repositoryRootRow, showRow);
|
||||
|
||||
if (showRow)
|
||||
{
|
||||
ViewDataBinder.UpdateToggle(_repositoryRootToggle, data.AddRepositoryRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateConfigError(EditorConfigData data)
|
||||
{
|
||||
bool hasError = !string.IsNullOrEmpty(data.ConfigurationError);
|
||||
ViewDataBinder.ToggleClass(_configErrorLabel, "mcp-error-message--visible", hasError);
|
||||
|
||||
if (hasError)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
_configErrorLabel.text = $"Error loading {editorName} configuration: {data.ConfigurationError}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateConfigureButton(EditorConfigData data)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
string buttonText;
|
||||
bool buttonEnabled = true;
|
||||
|
||||
if (data.IsChecking)
|
||||
{
|
||||
buttonText = $"Checking {editorName} Settings...";
|
||||
buttonEnabled = false;
|
||||
}
|
||||
else if (data.IsConfigured)
|
||||
{
|
||||
if (data.IsUpdateNeeded)
|
||||
{
|
||||
if (data.HasPortMismatch)
|
||||
{
|
||||
buttonText = data.IsServerRunning
|
||||
? $"Update {editorName} Settings\n(Port mismatch - Server: {data.CurrentPort})"
|
||||
: $"Update {editorName} Settings\n(Port mismatch)";
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText = data.IsServerRunning
|
||||
? $"Update {editorName} Settings\n(Port {data.CurrentPort})"
|
||||
: $"Update {editorName} Settings";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText = $"Settings Already Configured\n(Port {data.CurrentPort})";
|
||||
buttonEnabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText = $"Settings not found. \nConfigure {editorName}";
|
||||
}
|
||||
|
||||
_configureButton.text = buttonText;
|
||||
_configureButton.SetEnabled(buttonEnabled);
|
||||
|
||||
UpdateButtonStyle(data, buttonEnabled);
|
||||
}
|
||||
|
||||
private void UpdateButtonStyle(EditorConfigData data, bool buttonEnabled)
|
||||
{
|
||||
_configureButton.RemoveFromClassList("mcp-button--disabled");
|
||||
_configureButton.RemoveFromClassList("mcp-button--warning");
|
||||
|
||||
if (!buttonEnabled)
|
||||
{
|
||||
_configureButton.AddToClassList("mcp-button--disabled");
|
||||
}
|
||||
else if (!data.IsConfigured || data.HasPortMismatch)
|
||||
{
|
||||
_configureButton.AddToClassList("mcp-button--warning");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDeleteButton(EditorConfigData data)
|
||||
{
|
||||
ViewDataBinder.SetVisible(_deleteConfigButton, data.IsConfigured);
|
||||
|
||||
if (data.IsConfigured)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
_deleteConfigButton.text = $"Delete {editorName} Configuration";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOpenSettingsButton(EditorConfigData data)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
_openSettingsButton.text = $"Open {editorName} Settings File";
|
||||
}
|
||||
|
||||
private string GetEditorDisplayName(McpEditorType editorType)
|
||||
{
|
||||
return editorType switch
|
||||
{
|
||||
McpEditorType.Cursor => "Cursor",
|
||||
McpEditorType.ClaudeCode => "Claude Code",
|
||||
McpEditorType.VSCode => "VSCode",
|
||||
McpEditorType.GeminiCLI => "Gemini CLI",
|
||||
McpEditorType.Codex => "Codex",
|
||||
McpEditorType.McpInspector => "MCP Inspector",
|
||||
_ => editorType.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edb525b005f3540e5bc7ab48fcbec256
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section for server start/stop controls and port configuration.
|
||||
/// Part of McpEditorWindowUI's component hierarchy.
|
||||
/// </summary>
|
||||
public class ServerControlsSection
|
||||
{
|
||||
private readonly IntegerField _portField;
|
||||
private readonly VisualElement _portWarningContainer;
|
||||
private readonly Label _portWarningLabel;
|
||||
private readonly Button _toggleServerButton;
|
||||
|
||||
private ServerControlsData _lastData;
|
||||
|
||||
public event Action OnToggleServer;
|
||||
public event Action<int> OnPortChanged;
|
||||
|
||||
public ServerControlsSection(VisualElement root)
|
||||
{
|
||||
_portField = root.Q<IntegerField>("port-field");
|
||||
_portWarningContainer = root.Q<VisualElement>("port-warning-container");
|
||||
_portWarningLabel = root.Q<Label>("port-warning-label");
|
||||
_toggleServerButton = root.Q<Button>("toggle-server-button");
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_portField.RegisterValueChangedCallback(evt => OnPortChanged?.Invoke(evt.newValue));
|
||||
_toggleServerButton.clicked += () => OnToggleServer?.Invoke();
|
||||
}
|
||||
|
||||
public void Update(ServerControlsData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
ViewDataBinder.UpdateIntegerField(_portField, data.CustomPort);
|
||||
_portField.SetEnabled(!data.IsServerRunning);
|
||||
|
||||
UpdatePortWarning(data);
|
||||
UpdateToggleButton(data);
|
||||
}
|
||||
|
||||
private void UpdatePortWarning(ServerControlsData data)
|
||||
{
|
||||
bool showWarning = data.HasPortWarning && !string.IsNullOrEmpty(data.PortWarningMessage);
|
||||
ViewDataBinder.ToggleClass(_portWarningContainer, "mcp-warning-container--visible", showWarning);
|
||||
|
||||
if (showWarning)
|
||||
{
|
||||
_portWarningLabel.text = data.PortWarningMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleButton(ServerControlsData data)
|
||||
{
|
||||
if (data.IsServerRunning)
|
||||
{
|
||||
_toggleServerButton.text = "Stop Server";
|
||||
_toggleServerButton.RemoveFromClassList("mcp-button--start");
|
||||
_toggleServerButton.AddToClassList("mcp-button--stop");
|
||||
}
|
||||
else
|
||||
{
|
||||
_toggleServerButton.text = "Start Server";
|
||||
_toggleServerButton.RemoveFromClassList("mcp-button--stop");
|
||||
_toggleServerButton.AddToClassList("mcp-button--start");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 429dc1f90707e4aa1aa2d9fa8c92d4c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section displaying server running status.
|
||||
/// Part of McpEditorWindowUI's component hierarchy.
|
||||
/// </summary>
|
||||
public class ServerStatusSection
|
||||
{
|
||||
private readonly Label _statusValueLabel;
|
||||
private readonly VisualElement _statusIndicator;
|
||||
private ServerStatusData _lastData;
|
||||
|
||||
public ServerStatusSection(VisualElement root)
|
||||
{
|
||||
_statusValueLabel = root.Q<Label>("status-value");
|
||||
_statusIndicator = root.Q<VisualElement>("server-status-indicator");
|
||||
}
|
||||
|
||||
public void Update(ServerStatusData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
_statusValueLabel.text = data.Status;
|
||||
|
||||
_statusValueLabel.RemoveFromClassList("mcp-server-status__text--running");
|
||||
_statusValueLabel.RemoveFromClassList("mcp-server-status__text--stopped");
|
||||
_statusIndicator.RemoveFromClassList("mcp-server-status__indicator--running");
|
||||
_statusIndicator.RemoveFromClassList("mcp-server-status__indicator--stopped");
|
||||
|
||||
if (data.IsRunning)
|
||||
{
|
||||
_statusValueLabel.AddToClassList("mcp-server-status__text--running");
|
||||
_statusIndicator.AddToClassList("mcp-server-status__indicator--running");
|
||||
}
|
||||
else
|
||||
{
|
||||
_statusValueLabel.AddToClassList("mcp-server-status__text--stopped");
|
||||
_statusIndicator.AddToClassList("mcp-server-status__indicator--stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee3977cf4f2304e6c940d5b1d343a3e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,562 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section for tool permissions and a virtualized per-tool enable list.
|
||||
/// The expensive list is allowed to stay unloaded while the foldout is collapsed.
|
||||
/// </summary>
|
||||
public class ToolSettingsSection
|
||||
{
|
||||
private const int ToolListRowHeight = 24;
|
||||
private const int InlineToolRowLimit = 40;
|
||||
|
||||
private readonly Foldout _foldout;
|
||||
private readonly Toggle _allowThirdPartyToggle;
|
||||
private readonly Label _allowThirdPartyLabel;
|
||||
private readonly Button _securityLevelRestrictedButton;
|
||||
private readonly Button _securityLevelFullAccessButton;
|
||||
private readonly Label _securityLevelDescription;
|
||||
private readonly VisualElement _toolSettingsInfoContainer;
|
||||
private readonly VisualElement _toolListContainer;
|
||||
private readonly Label _toolListStatusLabel;
|
||||
private readonly ListView _toolListView;
|
||||
private readonly List<ToolListRowData> _toolListRows = new();
|
||||
private readonly Dictionary<string, Toggle> _togglesByToolName = new();
|
||||
private bool _allowThirdPartyTools = true;
|
||||
private bool _isRegistryAvailable;
|
||||
private bool _isUnavailableStateShown;
|
||||
private bool _isLoadingStateShown;
|
||||
private string _layoutSignature = string.Empty;
|
||||
|
||||
public event Action<bool> OnFoldoutChanged;
|
||||
public event Action<string, bool> OnToolToggled;
|
||||
public event Action<bool> OnAllowThirdPartyChanged;
|
||||
public event Action<DynamicCodeSecurityLevel> OnSecurityLevelChanged;
|
||||
|
||||
public ToolSettingsSection(VisualElement root)
|
||||
{
|
||||
_foldout = root.Q<Foldout>("tool-settings-foldout");
|
||||
_allowThirdPartyToggle = root.Q<Toggle>("allow-third-party-toggle");
|
||||
_allowThirdPartyLabel = root.Q<Label>("allow-third-party-label");
|
||||
_securityLevelRestrictedButton = root.Q<Button>("security-level-restricted-button");
|
||||
_securityLevelFullAccessButton = root.Q<Button>("security-level-full-access-button");
|
||||
_securityLevelDescription = root.Q<Label>("security-level-description");
|
||||
_toolSettingsInfoContainer = root.Q<VisualElement>("tool-settings-info-container");
|
||||
_toolListContainer = root.Q<VisualElement>("tool-list-container");
|
||||
Debug.Assert(_toolListContainer != null, "tool-list-container must not be null");
|
||||
|
||||
_toolListStatusLabel = CreateToolListStatusLabel();
|
||||
_toolListView = CreateToolListView();
|
||||
_toolListContainer.Add(_toolListStatusLabel);
|
||||
_toolListContainer.Add(_toolListView);
|
||||
ClearToolList();
|
||||
|
||||
Label cliReferenceLink = root.Q<Label>("cli-reference-link");
|
||||
if (cliReferenceLink != null)
|
||||
{
|
||||
cliReferenceLink.RegisterCallback<ClickEvent>(_ => Application.OpenURL(McpUIConstants.CLI_COMMAND_REFERENCE_URL));
|
||||
}
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_foldout.RegisterValueChangedCallback(evt => OnFoldoutChanged?.Invoke(evt.newValue));
|
||||
|
||||
_allowThirdPartyToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
OnAllowThirdPartyChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
|
||||
_allowThirdPartyLabel.RegisterCallback<ClickEvent>(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
bool newValue = !_allowThirdPartyToggle.value;
|
||||
_allowThirdPartyToggle.SetValueWithoutNotify(newValue);
|
||||
OnAllowThirdPartyChanged?.Invoke(newValue);
|
||||
});
|
||||
|
||||
_securityLevelRestrictedButton.clicked += () => UpdateSecurityLevel(DynamicCodeSecurityLevel.Restricted);
|
||||
_securityLevelFullAccessButton.clicked += () => UpdateSecurityLevel(DynamicCodeSecurityLevel.FullAccess);
|
||||
}
|
||||
|
||||
public void Update(ToolSettingsSectionData data)
|
||||
{
|
||||
_allowThirdPartyTools = data.AllowThirdPartyTools;
|
||||
|
||||
ViewDataBinder.UpdateFoldout(_foldout, data.ShowToolSettings);
|
||||
ViewDataBinder.UpdateToggle(_allowThirdPartyToggle, data.AllowThirdPartyTools);
|
||||
UpdateSecurityLevelSelection(data.DynamicCodeSecurityLevel);
|
||||
UpdateSecurityLevelDescription(data.DynamicCodeSecurityLevel);
|
||||
|
||||
if (!data.ShowToolSettings)
|
||||
{
|
||||
ClearToolList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.HasToolListData)
|
||||
{
|
||||
UpdateDeferredState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.IsRegistryAvailable)
|
||||
{
|
||||
UpdateUnavailableState();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateToolList(data);
|
||||
}
|
||||
|
||||
public void UpdateSingleToggle(string toolName, bool enabled)
|
||||
{
|
||||
for (int i = 0; i < _toolListRows.Count; i++)
|
||||
{
|
||||
ToolListRowData row = _toolListRows[i];
|
||||
if (row.IsHeader || row.ToolName != toolName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
row.IsEnabled = enabled;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_togglesByToolName.TryGetValue(toolName, out Toggle toggle))
|
||||
{
|
||||
toggle.SetValueWithoutNotify(enabled);
|
||||
}
|
||||
|
||||
RefreshToolListView();
|
||||
}
|
||||
|
||||
private void UpdateDeferredState()
|
||||
{
|
||||
if (_toolListRows.Count > 0 || _isUnavailableStateShown)
|
||||
{
|
||||
UpdateThirdPartyGroupState(_allowThirdPartyTools);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateLoadingState();
|
||||
}
|
||||
|
||||
private void UpdateLoadingState()
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
_layoutSignature = string.Empty;
|
||||
|
||||
SetToolListStatus("Loading tools...");
|
||||
ViewDataBinder.SetVisible(_toolListView, false);
|
||||
SetToolSettingsInfoVisible(false);
|
||||
|
||||
_isRegistryAvailable = false;
|
||||
_isUnavailableStateShown = false;
|
||||
_isLoadingStateShown = true;
|
||||
}
|
||||
|
||||
private void UpdateUnavailableState()
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
_layoutSignature = string.Empty;
|
||||
|
||||
SetToolListStatus("Tool registry not yet initialized. Start the server first.");
|
||||
ViewDataBinder.SetVisible(_toolListView, false);
|
||||
SetToolSettingsInfoVisible(false);
|
||||
|
||||
_isRegistryAvailable = false;
|
||||
_isUnavailableStateShown = true;
|
||||
_isLoadingStateShown = false;
|
||||
}
|
||||
|
||||
private void ClearToolList()
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
_layoutSignature = string.Empty;
|
||||
|
||||
ViewDataBinder.SetVisible(_toolListStatusLabel, false);
|
||||
ViewDataBinder.SetVisible(_toolListView, false);
|
||||
SetToolSettingsInfoVisible(false);
|
||||
RefreshToolListView();
|
||||
|
||||
_isRegistryAvailable = false;
|
||||
_isUnavailableStateShown = false;
|
||||
_isLoadingStateShown = false;
|
||||
}
|
||||
|
||||
private void SetToolListStatus(string text)
|
||||
{
|
||||
_toolListStatusLabel.text = text;
|
||||
ViewDataBinder.SetVisible(_toolListStatusLabel, true);
|
||||
}
|
||||
|
||||
private void SetToolSettingsInfoVisible(bool visible)
|
||||
{
|
||||
if (_toolSettingsInfoContainer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewDataBinder.SetVisible(_toolSettingsInfoContainer, visible);
|
||||
}
|
||||
|
||||
private void UpdateToolList(ToolSettingsSectionData data)
|
||||
{
|
||||
string layoutSignature = CreateLayoutSignature(data);
|
||||
bool shouldRebuild = !_isRegistryAvailable
|
||||
|| _isUnavailableStateShown
|
||||
|| _isLoadingStateShown
|
||||
|| _layoutSignature != layoutSignature;
|
||||
|
||||
if (shouldRebuild)
|
||||
{
|
||||
Rebuild(data);
|
||||
_layoutSignature = layoutSignature;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateToggleStates(data.BuiltInTools);
|
||||
UpdateToggleStates(data.ThirdPartyTools);
|
||||
}
|
||||
|
||||
ViewDataBinder.SetVisible(_toolListStatusLabel, false);
|
||||
ViewDataBinder.SetVisible(_toolListView, true);
|
||||
SetToolSettingsInfoVisible(true);
|
||||
UpdateThirdPartyGroupState(data.AllowThirdPartyTools);
|
||||
|
||||
_isRegistryAvailable = true;
|
||||
_isUnavailableStateShown = false;
|
||||
_isLoadingStateShown = false;
|
||||
}
|
||||
|
||||
private void UpdateSecurityLevelDescription(DynamicCodeSecurityLevel currentLevel)
|
||||
{
|
||||
string description = currentLevel switch
|
||||
{
|
||||
DynamicCodeSecurityLevel.Restricted => "Dangerous APIs blocked (recommended)",
|
||||
DynamicCodeSecurityLevel.FullAccess => "All APIs available (use with caution)",
|
||||
_ => "Unknown level"
|
||||
};
|
||||
|
||||
_securityLevelDescription.text = description;
|
||||
_securityLevelDescription.RemoveFromClassList("mcp-security-level-description--warning");
|
||||
|
||||
if (currentLevel == DynamicCodeSecurityLevel.FullAccess)
|
||||
{
|
||||
_securityLevelDescription.AddToClassList("mcp-security-level-description--warning");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSecurityLevel(DynamicCodeSecurityLevel newLevel)
|
||||
{
|
||||
UpdateSecurityLevelSelection(newLevel);
|
||||
UpdateSecurityLevelDescription(newLevel);
|
||||
OnSecurityLevelChanged?.Invoke(newLevel);
|
||||
}
|
||||
|
||||
private void UpdateSecurityLevelSelection(DynamicCodeSecurityLevel currentLevel)
|
||||
{
|
||||
bool isRestricted = currentLevel == DynamicCodeSecurityLevel.Restricted;
|
||||
bool isFullAccess = currentLevel == DynamicCodeSecurityLevel.FullAccess;
|
||||
|
||||
ViewDataBinder.ToggleClass(_securityLevelRestrictedButton, "mcp-segmented-control__button--active", isRestricted);
|
||||
ViewDataBinder.ToggleClass(_securityLevelRestrictedButton, "mcp-segmented-control__button--warning-active", false);
|
||||
ViewDataBinder.ToggleClass(_securityLevelFullAccessButton, "mcp-segmented-control__button--active", isFullAccess);
|
||||
ViewDataBinder.ToggleClass(_securityLevelFullAccessButton, "mcp-segmented-control__button--warning-active", isFullAccess);
|
||||
}
|
||||
|
||||
private void Rebuild(ToolSettingsSectionData data)
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
|
||||
if (data.BuiltInTools.Length > 0)
|
||||
{
|
||||
_toolListRows.Add(ToolListRowData.CreateHeader("Built-in Tools", false));
|
||||
AddToolRows(data.BuiltInTools);
|
||||
}
|
||||
|
||||
if (data.ThirdPartyTools.Length > 0)
|
||||
{
|
||||
_toolListRows.Add(ToolListRowData.CreateHeader("Third Party Tools", true));
|
||||
AddToolRows(data.ThirdPartyTools);
|
||||
}
|
||||
|
||||
UpdateToolListHeight();
|
||||
RefreshToolListView();
|
||||
}
|
||||
|
||||
private void AddToolRows(IReadOnlyList<ToolToggleItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ToolToggleItem item = items[i];
|
||||
_toolListRows.Add(ToolListRowData.CreateTool(item));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleStates(IReadOnlyList<ToolToggleItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ToolToggleItem item = items[i];
|
||||
UpdateToggleState(item.ToolName, item.IsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleState(string toolName, bool isEnabled)
|
||||
{
|
||||
for (int i = 0; i < _toolListRows.Count; i++)
|
||||
{
|
||||
ToolListRowData row = _toolListRows[i];
|
||||
if (row.IsHeader || row.ToolName != toolName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
row.IsEnabled = isEnabled;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateLayoutSignature(ToolSettingsSectionData data)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
AppendGroupSignature(builder, data.BuiltInTools, "B");
|
||||
AppendGroupSignature(builder, data.ThirdPartyTools, "T");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendGroupSignature(StringBuilder builder, IReadOnlyList<ToolToggleItem> items, string group)
|
||||
{
|
||||
builder.Append(group);
|
||||
builder.Append(':');
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ToolToggleItem item = items[i];
|
||||
builder.Append(item.ToolName);
|
||||
builder.Append('|');
|
||||
}
|
||||
|
||||
builder.Append(';');
|
||||
}
|
||||
|
||||
private void UpdateThirdPartyGroupState(bool allowThirdPartyTools)
|
||||
{
|
||||
_allowThirdPartyTools = allowThirdPartyTools;
|
||||
RefreshToolListView();
|
||||
}
|
||||
|
||||
private static Label CreateToolListStatusLabel()
|
||||
{
|
||||
Label label = new Label();
|
||||
label.name = "tool-list-status-label";
|
||||
label.AddToClassList("mcp-tool-registry-unavailable");
|
||||
return label;
|
||||
}
|
||||
|
||||
private ListView CreateToolListView()
|
||||
{
|
||||
ListView listView = new ListView();
|
||||
listView.name = "tool-list-view";
|
||||
listView.AddToClassList("mcp-tool-list-view");
|
||||
listView.fixedItemHeight = ToolListRowHeight;
|
||||
listView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
|
||||
listView.selectionType = SelectionType.None;
|
||||
listView.itemsSource = _toolListRows;
|
||||
listView.makeItem = CreateToolListRowElement;
|
||||
listView.bindItem = BindToolListRowElement;
|
||||
listView.unbindItem = UnbindToolListRowElement;
|
||||
return listView;
|
||||
}
|
||||
|
||||
private static VisualElement CreateToolListRowElement()
|
||||
{
|
||||
VisualElement row = new VisualElement();
|
||||
row.AddToClassList("mcp-tool-toggle-row");
|
||||
row.AddToClassList("mcp-tool-list-row");
|
||||
|
||||
Toggle toggle = new Toggle();
|
||||
toggle.name = "tool-list-row-toggle";
|
||||
toggle.AddToClassList("mcp-tool-toggle-row__toggle");
|
||||
toggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
|
||||
if (row.userData is not ToolListRowData item || item.IsHeader)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.Owner?.OnToolToggled?.Invoke(item.ToolName, evt.newValue);
|
||||
});
|
||||
|
||||
Label label = new Label();
|
||||
label.name = "tool-list-row-label";
|
||||
label.AddToClassList("mcp-tool-toggle-row__label");
|
||||
label.RegisterCallback<ClickEvent>(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
|
||||
if (row.userData is not ToolListRowData item || item.IsHeader || !item.CanToggle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Toggle rowToggle = row.Q<Toggle>("tool-list-row-toggle");
|
||||
bool newValue = !rowToggle.value;
|
||||
rowToggle.SetValueWithoutNotify(newValue);
|
||||
item.Owner?.OnToolToggled?.Invoke(item.ToolName, newValue);
|
||||
});
|
||||
|
||||
row.Add(toggle);
|
||||
row.Add(label);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void BindToolListRowElement(VisualElement row, int index)
|
||||
{
|
||||
Debug.Assert(index >= 0 && index < _toolListRows.Count, "tool list index must be valid");
|
||||
|
||||
ToolListRowData item = _toolListRows[index];
|
||||
item.Owner = this;
|
||||
item.CanToggle = !item.IsThirdParty || _allowThirdPartyTools;
|
||||
row.userData = item;
|
||||
|
||||
Toggle toggle = row.Q<Toggle>("tool-list-row-toggle");
|
||||
Label label = row.Q<Label>("tool-list-row-label");
|
||||
Debug.Assert(toggle != null, "tool-list-row-toggle must not be null");
|
||||
Debug.Assert(label != null, "tool-list-row-label must not be null");
|
||||
|
||||
ResetRowClasses(row, label);
|
||||
|
||||
if (item.IsHeader)
|
||||
{
|
||||
BindHeaderRow(row, toggle, label, item);
|
||||
return;
|
||||
}
|
||||
|
||||
BindToolRow(row, toggle, label, item);
|
||||
}
|
||||
|
||||
private void BindHeaderRow(VisualElement row, Toggle toggle, Label label, ToolListRowData item)
|
||||
{
|
||||
ViewDataBinder.SetVisible(toggle, false);
|
||||
label.text = item.Label;
|
||||
label.tooltip = string.Empty;
|
||||
row.SetEnabled(true);
|
||||
row.AddToClassList("mcp-tool-list-row--header");
|
||||
label.AddToClassList("mcp-tool-group-header");
|
||||
ViewDataBinder.ToggleClass(row, "mcp-tool-list-row--disabled", item.IsThirdParty && !_allowThirdPartyTools);
|
||||
}
|
||||
|
||||
private void BindToolRow(VisualElement row, Toggle toggle, Label label, ToolListRowData item)
|
||||
{
|
||||
ViewDataBinder.SetVisible(toggle, true);
|
||||
toggle.SetValueWithoutNotify(item.IsEnabled);
|
||||
label.text = item.ToolName;
|
||||
label.tooltip = item.Description;
|
||||
|
||||
row.SetEnabled(item.CanToggle);
|
||||
ViewDataBinder.ToggleClass(row, "mcp-tool-list-row--disabled", !item.CanToggle);
|
||||
_togglesByToolName[item.ToolName] = toggle;
|
||||
}
|
||||
|
||||
private static void ResetRowClasses(VisualElement row, Label label)
|
||||
{
|
||||
row.RemoveFromClassList("mcp-tool-list-row--header");
|
||||
row.RemoveFromClassList("mcp-tool-list-row--disabled");
|
||||
label.RemoveFromClassList("mcp-tool-group-header");
|
||||
}
|
||||
|
||||
private void UnbindToolListRowElement(VisualElement row, int index)
|
||||
{
|
||||
if (row.userData is ToolListRowData item && !item.IsHeader)
|
||||
{
|
||||
_togglesByToolName.Remove(item.ToolName);
|
||||
}
|
||||
|
||||
row.userData = null;
|
||||
}
|
||||
|
||||
private void RefreshToolListView()
|
||||
{
|
||||
_togglesByToolName.Clear();
|
||||
_toolListView.RefreshItems();
|
||||
}
|
||||
|
||||
private void UpdateToolListHeight()
|
||||
{
|
||||
int visibleRows = Math.Min(_toolListRows.Count, InlineToolRowLimit);
|
||||
if (visibleRows <= 0)
|
||||
{
|
||||
visibleRows = 1;
|
||||
}
|
||||
|
||||
_toolListView.style.height = (visibleRows * ToolListRowHeight) + 2;
|
||||
}
|
||||
|
||||
private sealed class ToolListRowData
|
||||
{
|
||||
public readonly bool IsHeader;
|
||||
public readonly string ToolName;
|
||||
public readonly string Label;
|
||||
public readonly string Description;
|
||||
public readonly bool IsThirdParty;
|
||||
public bool IsEnabled;
|
||||
public bool CanToggle = true;
|
||||
public ToolSettingsSection Owner;
|
||||
|
||||
private ToolListRowData(
|
||||
bool isHeader,
|
||||
string toolName,
|
||||
string label,
|
||||
string description,
|
||||
bool isEnabled,
|
||||
bool isThirdParty)
|
||||
{
|
||||
IsHeader = isHeader;
|
||||
ToolName = toolName;
|
||||
Label = label;
|
||||
Description = description;
|
||||
IsEnabled = isEnabled;
|
||||
IsThirdParty = isThirdParty;
|
||||
}
|
||||
|
||||
public static ToolListRowData CreateHeader(string label, bool isThirdParty)
|
||||
{
|
||||
return new ToolListRowData(
|
||||
true,
|
||||
string.Empty,
|
||||
label,
|
||||
string.Empty,
|
||||
true,
|
||||
isThirdParty);
|
||||
}
|
||||
|
||||
public static ToolListRowData CreateTool(ToolToggleItem item)
|
||||
{
|
||||
return new ToolListRowData(
|
||||
false,
|
||||
item.ToolName,
|
||||
item.ToolName,
|
||||
item.Description,
|
||||
item.IsEnabled,
|
||||
item.IsThirdParty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91dea545df5d649e384a2d4268db5822
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,748 @@
|
||||
/* ===========================================
|
||||
McpEditorWindow UI Toolkit Stylesheet
|
||||
Using BEM naming convention and Unity theme variables
|
||||
=========================================== */
|
||||
|
||||
:root {
|
||||
--mcp-color-success: #4caf50;
|
||||
--mcp-color-danger: #ef5350;
|
||||
--mcp-color-warning: #ffd200;
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.mcp-window-root {
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.mcp-main-container {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mcp-main-container--debug {
|
||||
background-color: rgba(128, 0, 128, 0.15);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Section
|
||||
=========================================== */
|
||||
.mcp-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-section--box {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Server Status (prominent status row)
|
||||
=========================================== */
|
||||
.mcp-server-status {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator--running {
|
||||
background-color: var(--mcp-color-success);
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator--stopped {
|
||||
background-color: var(--mcp-color-danger);
|
||||
}
|
||||
|
||||
.mcp-server-status__text {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.mcp-server-status__text--running {
|
||||
color: var(--mcp-color-success);
|
||||
border-color: var(--mcp-color-success);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.mcp-server-status__text--stopped {
|
||||
color: var(--mcp-color-danger);
|
||||
border-color: var(--mcp-color-danger);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.mcp-server-status__spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-server-status__port-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-server-status__port-field {
|
||||
width: 60px;
|
||||
-unity-text-align: middle-right;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Server Controls (compact row)
|
||||
=========================================== */
|
||||
.mcp-server-controls {
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.mcp-server-controls .mcp-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Warning Container
|
||||
=========================================== */
|
||||
.mcp-warning-container {
|
||||
display: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-warning-container--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-warning-label {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Button
|
||||
=========================================== */
|
||||
.mcp-button {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-button--compact {
|
||||
height: 22px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mcp-button--start {
|
||||
background-color: #2e7d32;
|
||||
border-color: var(--mcp-color-success);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mcp-button--start:hover {
|
||||
background-color: #388e3c;
|
||||
}
|
||||
|
||||
.mcp-button--stop {
|
||||
background-color: #4a4a4a;
|
||||
border-color: #666666;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.mcp-button--stop:hover {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
.mcp-button--warning {
|
||||
background-color: #665522;
|
||||
border-color: #aa9944;
|
||||
}
|
||||
|
||||
.mcp-button--warning:hover {
|
||||
background-color: #887744;
|
||||
}
|
||||
|
||||
.mcp-button--configure {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#install-cli-button {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mcp-button--delete {
|
||||
margin-top: 12px;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.mcp-button--delete:hover {
|
||||
color: #ff8a8a;
|
||||
}
|
||||
|
||||
.mcp-button--disabled {
|
||||
background-color: #4a4a4a;
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.5);
|
||||
color: #999999;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Toggle Row
|
||||
=========================================== */
|
||||
.mcp-toggle-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-toggle-row__toggle {
|
||||
width: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-toggle-row__label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-toggle-row__label--danger {
|
||||
color: var(--mcp-color-warning);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Target Row (Editor Selection)
|
||||
=========================================== */
|
||||
.mcp-target-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mcp-target-row__label {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcp-target-row__field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Foldout
|
||||
=========================================== */
|
||||
.mcp-foldout {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
#tool-settings-foldout > .unity-foldout__content {
|
||||
margin-top: 2px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Client (Connected Tools Item)
|
||||
=========================================== */
|
||||
.mcp-clients-list {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mcp-client {
|
||||
background-color: rgba(76, 76, 76, 0.5);
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.5);
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
margin-bottom: 6px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mcp-client__icon {
|
||||
color: var(--mcp-color-success);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-client__name {
|
||||
-unity-font-style: bold;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-client__port {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Messages
|
||||
=========================================== */
|
||||
.mcp-info-message {
|
||||
display: none;
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mcp-info-message--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-warning-message {
|
||||
display: none;
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mcp-warning-message--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-error-message {
|
||||
display: none;
|
||||
background-color: var(--unity-colors-error-background, #440000);
|
||||
border-color: var(--unity-colors-error-border, #aa0000);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
white-space: normal;
|
||||
color: var(--unity-colors-error-text, #ff4444);
|
||||
}
|
||||
|
||||
.mcp-error-message--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Tool Settings
|
||||
=========================================== */
|
||||
.mcp-tool-settings-info {
|
||||
white-space: normal;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-note {
|
||||
font-size: 10px;
|
||||
color: #888888;
|
||||
white-space: normal;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-link {
|
||||
font-size: 10px;
|
||||
color: #4C7EF3;
|
||||
white-space: normal;
|
||||
margin-bottom: 8px;
|
||||
cursor: link;
|
||||
-unity-font-style: italic;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-link:hover {
|
||||
color: #6B9AFF;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-content {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#tool-settings-info-container {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-card {
|
||||
border-width: 1px;
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
padding: 0px 8px 8px 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-card__header {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mcp-tool-group-header {
|
||||
-unity-font-style: bold;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-tool-group--disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.mcp-permission-item {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0 0 14px;
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.mcp-permission-item--first {
|
||||
margin-top: 0;
|
||||
padding-top: 4px;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.mcp-permission-item__title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
color: var(--mcp-color-warning);
|
||||
}
|
||||
|
||||
.mcp-permission-item__title--dynamic-code {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mcp-permission-item__control-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mcp-permission-item__control-label {
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.mcp-permission-item__toggle-row {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mcp-permission-item__assistive-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mcp-segmented-control {
|
||||
flex-direction: row;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
background-color: rgba(48, 48, 48, 0.6);
|
||||
border-radius: 3px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button {
|
||||
flex-grow: 1;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
-unity-font-style: normal;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button:hover {
|
||||
background-color: rgba(46, 90, 136, 0.3);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
#security-level-full-access-button:hover {
|
||||
background-color: rgba(122, 39, 39, 0.35);
|
||||
color: #ffe3e3;
|
||||
border-width: 1px;
|
||||
border-color: rgba(178, 76, 76, 0.55);
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button--active {
|
||||
background-color: rgba(46, 90, 136, 0.8);
|
||||
color: #ffffff;
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button--warning-active {
|
||||
background-color: rgba(122, 39, 39, 0.85);
|
||||
color: #ffd7d7;
|
||||
border-width: 1px;
|
||||
border-color: rgba(178, 76, 76, 0.7);
|
||||
}
|
||||
|
||||
.mcp-tool-toggle-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 2px 4px 2px 14px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.mcp-tool-toggle-row__toggle {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-tool-toggle-row__label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-tool-list-container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.mcp-tool-list-view {
|
||||
margin-top: 4px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mcp-tool-list-row {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.mcp-tool-list-row--header {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mcp-tool-list-row--disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.mcp-tool-registry-unavailable {
|
||||
color: #888888;
|
||||
-unity-font-style: italic;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Security
|
||||
=========================================== */
|
||||
.mcp-security-warning {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
white-space: normal;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.mcp-section-header {
|
||||
-unity-font-style: bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-section-header--danger {
|
||||
color: var(--mcp-color-warning);
|
||||
}
|
||||
|
||||
.mcp-security-level-field {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-security-level-description {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 6px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mcp-security-level-description--warning {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Tab Toggle (segment control)
|
||||
=========================================== */
|
||||
.mcp-tab-toggle {
|
||||
flex-direction: row;
|
||||
background-color: rgba(48, 48, 48, 0.6);
|
||||
border-radius: 3px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.2);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.mcp-tab-button {
|
||||
flex-grow: 1;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
-unity-font-style: normal;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mcp-tab-button:hover {
|
||||
background-color: rgba(46, 90, 136, 0.3);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
.mcp-tab-button--active {
|
||||
background-color: rgba(46, 90, 136, 0.8);
|
||||
color: #ffffff;
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
.mcp-tab-button--active:hover {
|
||||
background-color: rgba(46, 90, 136, 0.8);
|
||||
color: #ffffff;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: CLI Setup
|
||||
=========================================== */
|
||||
.mcp-cli-subsection-header {
|
||||
-unity-font-style: bold;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-cli-separator {
|
||||
height: 1px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 6px;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.mcp-cli-status-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#cli-status-label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-cli-status-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mcp-cli-status-icon--installed {
|
||||
background-color: var(--mcp-color-success);
|
||||
}
|
||||
|
||||
.mcp-cli-status-icon--not-installed {
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
|
||||
.mcp-button--refresh {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
margin: 0 3px 0 4px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
-unity-text-align: middle-center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: GitHub Link
|
||||
=========================================== */
|
||||
.mcp-github-link {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding-top: 4px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 4px;
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
margin-right: 6px;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.mcp-github-link__label {
|
||||
font-size: 11px;
|
||||
color: #c3cad3;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mcp-github-link__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.78;
|
||||
-unity-background-scale-mode: scale-to-fit;
|
||||
}
|
||||
|
||||
.mcp-github-link--hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mcp-github-link__label--hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mcp-github-link__icon--hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Utility: Hidden
|
||||
=========================================== */
|
||||
.mcp-hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b160f81152d6c4270bb39f5ba88ba7ed
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
<ui:VisualElement name="window-root" class="mcp-window-root">
|
||||
<ui:ScrollView name="main-scroll-view" class="mcp-main-container">
|
||||
<!-- Configuration Section -->
|
||||
<ui:VisualElement name="configuration-section" class="mcp-section mcp-section--box">
|
||||
<ui:Foldout name="configuration-foldout" text="Configuration" class="mcp-foldout">
|
||||
<ui:VisualElement class="mcp-tab-toggle">
|
||||
<ui:Button name="cli-tab-button" text="CLI" class="mcp-tab-button mcp-tab-button--active" />
|
||||
<ui:Button name="mcp-tab-button" text="MCP" class="mcp-tab-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- CLI Content -->
|
||||
<ui:VisualElement name="cli-content">
|
||||
<ui:VisualElement name="project-cli-version-row" class="mcp-toggle-row">
|
||||
<ui:Toggle name="project-cli-version-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label
|
||||
name="project-cli-version-label"
|
||||
text="Use project CLI version"
|
||||
tooltip="Run installed skills through the Unity package version of uloop-cli."
|
||||
class="mcp-toggle-row__label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="global-cli-status-row" class="mcp-cli-status-row">
|
||||
<ui:VisualElement name="cli-status-icon" class="mcp-cli-status-icon mcp-cli-status-icon--not-installed" />
|
||||
<ui:Label name="cli-status-label" text="uloop-cli: Checking..." />
|
||||
<ui:Button name="refresh-cli-version-button" text="↻" class="mcp-button--refresh" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="install-cli-button" text="Install CLI" class="mcp-button mcp-button--configure" />
|
||||
<ui:VisualElement name="global-cli-separator" class="mcp-cli-separator" />
|
||||
|
||||
<ui:VisualElement name="skills-subsection">
|
||||
<ui:Label text="Skills" class="mcp-cli-subsection-header" />
|
||||
<ui:VisualElement name="group-skills-row" class="mcp-toggle-row">
|
||||
<ui:Toggle name="group-skills-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label
|
||||
name="group-skills-label"
|
||||
text="Group in subfolder"
|
||||
class="mcp-toggle-row__label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="mcp-target-row">
|
||||
<ui:Label text="Target:" class="mcp-target-row__label" />
|
||||
<uie:EnumField name="skills-target-field" class="mcp-target-row__field" />
|
||||
<ui:Button name="refresh-skills-state-button" text="↻" class="mcp-button--refresh" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="install-skills-button" text="Install Skills" class="mcp-button mcp-button--configure" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- MCP Content -->
|
||||
<ui:VisualElement name="mcp-content">
|
||||
<ui:VisualElement class="mcp-warning-container mcp-warning-container--visible">
|
||||
<ui:Label
|
||||
text="This MCP configuration is already deprecated. Please use the CLI instead. The MCP feature will be removed in a future release."
|
||||
class="mcp-warning-label" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="mcp-target-row">
|
||||
<ui:Label text="Target:" class="mcp-target-row__label" />
|
||||
<uie:EnumField name="editor-type-field" class="mcp-target-row__field" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="repository-root-row" class="mcp-toggle-row">
|
||||
<ui:Toggle name="repository-root-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label text="Use Repository Root" class="mcp-toggle-row__label" tooltip="When enabled, generates configuration file at the Git repository root" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:Label name="config-error-label" class="mcp-error-message" />
|
||||
|
||||
<ui:Button name="configure-button" text="Configure" class="mcp-button mcp-button--configure" />
|
||||
<ui:Button name="open-settings-button" text="Open Settings File" class="mcp-button" />
|
||||
<ui:Button name="delete-config-button" text="Delete Configuration" class="mcp-button mcp-button--delete" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Connected LLM Tools Section -->
|
||||
<ui:VisualElement name="connected-tools-section" class="mcp-section mcp-section--box">
|
||||
<ui:Foldout name="connected-tools-foldout" text="Connected LLM Tools (via MCP)" class="mcp-foldout">
|
||||
<ui:VisualElement name="connected-tools-content">
|
||||
<ui:VisualElement name="clients-list" class="mcp-clients-list" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Tool Settings Section -->
|
||||
<ui:VisualElement name="tool-settings-section" class="mcp-section mcp-section--box">
|
||||
<ui:Foldout name="tool-settings-foldout" text="Tool Settings" class="mcp-foldout">
|
||||
<ui:VisualElement name="tool-settings-content" class="mcp-tool-settings-content">
|
||||
<ui:VisualElement class="mcp-tool-settings-card">
|
||||
<ui:Label text="Permissions" class="mcp-section-header mcp-tool-settings-card__header" />
|
||||
<ui:VisualElement class="mcp-permission-item mcp-permission-item--first">
|
||||
<ui:Label text="Allow Third Party Tools" class="mcp-permission-item__title mcp-section-header--danger" />
|
||||
<ui:VisualElement class="mcp-permission-item__control-row">
|
||||
<ui:VisualElement class="mcp-toggle-row mcp-permission-item__toggle-row">
|
||||
<ui:Toggle name="allow-third-party-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label text="Enable" class="mcp-permission-item__control-label" />
|
||||
<ui:Label name="allow-third-party-label" text="Allow Third Party Tools" class="mcp-toggle-row__label mcp-toggle-row__label--danger mcp-permission-item__assistive-label" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="mcp-permission-item">
|
||||
<ui:Label text="execute-dynamic-code" class="mcp-permission-item__title mcp-permission-item__title--dynamic-code mcp-section-header--danger" />
|
||||
<ui:Label text="Security Level" class="mcp-permission-item__control-label" />
|
||||
<ui:VisualElement class="mcp-segmented-control">
|
||||
<ui:Button name="security-level-restricted-button" text="Restricted" class="mcp-segmented-control__button" />
|
||||
<ui:Button name="security-level-full-access-button" text="Full Access" class="mcp-segmented-control__button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="security-level-description" class="mcp-security-level-description" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="tool-settings-info-container">
|
||||
<ui:Label text="Enable or disable individual tools. Disabled tools are completely hidden from AI agents, eliminating unnecessary context window consumption." class="mcp-tool-settings-info" />
|
||||
<ui:Label text="Changes are notified to connected MCP clients immediately." class="mcp-tool-settings-note" />
|
||||
<ui:Label name="cli-reference-link" text="See CLI Command Reference for uloop-cli usage." class="mcp-tool-settings-link" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="tool-list-container" class="mcp-tool-list-container" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
</ui:ScrollView>
|
||||
|
||||
<ui:VisualElement name="github-link-row" class="mcp-github-link">
|
||||
<ui:Label name="github-link-label" text="Star on GitHub" class="mcp-github-link__label" />
|
||||
<ui:Image name="github-link-icon" class="mcp-github-link__icon" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 718e4ecdb8013474282d23c964d9f98d
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// View layer for McpEditorWindow in MVP architecture.
|
||||
/// Owns UI sections and forwards user interactions to presenter via events.
|
||||
/// Related: McpEditorWindow (presenter), McpEditorModel (model)
|
||||
/// </summary>
|
||||
public class McpEditorWindowUI : IDisposable
|
||||
{
|
||||
private const string UXML_RELATIVE_PATH = "Editor/UI/UIToolkit/McpEditorWindow.uxml";
|
||||
private const string USS_RELATIVE_PATH = "Editor/UI/UIToolkit/McpEditorWindow.uss";
|
||||
private const string GITHUB_ICON_RELATIVE_PATH = "Editor/UI/Setup/GitHub_Invertocat_White.png";
|
||||
|
||||
private readonly VisualElement _root;
|
||||
|
||||
private ConnectionModeSection _connectionModeSection;
|
||||
private CliSetupSection _cliSetupSection;
|
||||
private ConnectedToolsSection _connectedToolsSection;
|
||||
private EditorConfigSection _editorConfigSection;
|
||||
private ToolSettingsSection _toolSettingsSection;
|
||||
|
||||
private VisualElement _cliContent;
|
||||
private VisualElement _mcpContent;
|
||||
private ScrollView _mainScrollView;
|
||||
private VisualElement _githubLinkRow;
|
||||
private Label _githubLinkLabel;
|
||||
private Image _githubLinkIcon;
|
||||
|
||||
public event Action<ConnectionMode> OnConnectionModeChanged;
|
||||
public event Action OnRefreshCliVersion;
|
||||
public event Action OnInstallCli;
|
||||
public event Action OnInstallSkills;
|
||||
public event Action OnRefreshSkillsState;
|
||||
public event Action<SkillsTarget> OnSkillsTargetChanged;
|
||||
public event Action<bool> OnGroupSkillsChanged;
|
||||
public event Action<bool> OnUseProjectCliVersionChanged;
|
||||
public event Action<bool> OnConfigurationFoldoutChanged;
|
||||
public event Action<bool> OnConnectedToolsFoldoutChanged;
|
||||
public event Action<McpEditorType> OnEditorTypeChanged;
|
||||
public event Action<bool> OnRepositoryRootChanged;
|
||||
public event Action OnConfigureClicked;
|
||||
public event Action OnDeleteConfigClicked;
|
||||
public event Action OnOpenSettingsClicked;
|
||||
public event Action<bool> OnToolSettingsFoldoutChanged;
|
||||
public event Action<string, bool> OnToolToggled;
|
||||
public event Action<bool> OnAllowThirdPartyChanged;
|
||||
public event Action<DynamicCodeSecurityLevel> OnSecurityLevelChanged;
|
||||
|
||||
public McpEditorWindowUI(VisualElement root)
|
||||
{
|
||||
_root = root;
|
||||
LoadLayout();
|
||||
ApplyDebugStyle();
|
||||
InitializeSections();
|
||||
}
|
||||
|
||||
private void ApplyDebugStyle()
|
||||
{
|
||||
#if ULOOPMCP_DEBUG
|
||||
VisualElement mainContainer = _root.Q<VisualElement>("main-scroll-view");
|
||||
mainContainer?.AddToClassList("mcp-main-container--debug");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void LoadLayout()
|
||||
{
|
||||
string uxmlPath = $"{McpConstants.PackageAssetPath}/{UXML_RELATIVE_PATH}";
|
||||
string ussPath = $"{McpConstants.PackageAssetPath}/{USS_RELATIVE_PATH}";
|
||||
|
||||
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
|
||||
if (visualTree == null)
|
||||
{
|
||||
Debug.LogError($"Failed to load UXML from: {uxmlPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
visualTree.CloneTree(_root);
|
||||
|
||||
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
|
||||
if (styleSheet == null)
|
||||
{
|
||||
Debug.LogError($"Failed to load USS from: {ussPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
_root.styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
private void InitializeSections()
|
||||
{
|
||||
_connectionModeSection = new ConnectionModeSection(_root);
|
||||
_connectionModeSection.OnModeChanged += mode => OnConnectionModeChanged?.Invoke(mode);
|
||||
_connectionModeSection.OnFoldoutChanged += value => OnConfigurationFoldoutChanged?.Invoke(value);
|
||||
|
||||
_cliSetupSection = new CliSetupSection(_root);
|
||||
_cliSetupSection.SetupBindings();
|
||||
_cliSetupSection.OnRefreshCliVersion += () => OnRefreshCliVersion?.Invoke();
|
||||
_cliSetupSection.OnInstallCli += () => OnInstallCli?.Invoke();
|
||||
_cliSetupSection.OnInstallSkills += () => OnInstallSkills?.Invoke();
|
||||
_cliSetupSection.OnRefreshSkillsState += () => OnRefreshSkillsState?.Invoke();
|
||||
_cliSetupSection.OnSkillsTargetChanged += value => OnSkillsTargetChanged?.Invoke(value);
|
||||
_cliSetupSection.OnGroupSkillsChanged += value => OnGroupSkillsChanged?.Invoke(value);
|
||||
_cliSetupSection.OnUseProjectCliVersionChanged += value => OnUseProjectCliVersionChanged?.Invoke(value);
|
||||
|
||||
_mainScrollView = _root.Q<ScrollView>("main-scroll-view");
|
||||
ConfigureScrollView();
|
||||
|
||||
_cliContent = _root.Q<VisualElement>("cli-content");
|
||||
_mcpContent = _root.Q<VisualElement>("mcp-content");
|
||||
|
||||
_connectedToolsSection = new ConnectedToolsSection(_root);
|
||||
_connectedToolsSection.OnFoldoutChanged += value => OnConnectedToolsFoldoutChanged?.Invoke(value);
|
||||
|
||||
_editorConfigSection = new EditorConfigSection(_root);
|
||||
_editorConfigSection.OnEditorTypeChanged += value => OnEditorTypeChanged?.Invoke(value);
|
||||
_editorConfigSection.OnRepositoryRootChanged += value => OnRepositoryRootChanged?.Invoke(value);
|
||||
_editorConfigSection.OnConfigureClicked += () => OnConfigureClicked?.Invoke();
|
||||
_editorConfigSection.OnDeleteConfigClicked += () => OnDeleteConfigClicked?.Invoke();
|
||||
_editorConfigSection.OnOpenSettingsClicked += () => OnOpenSettingsClicked?.Invoke();
|
||||
|
||||
_toolSettingsSection = new ToolSettingsSection(_root);
|
||||
_toolSettingsSection.OnFoldoutChanged += value => OnToolSettingsFoldoutChanged?.Invoke(value);
|
||||
_toolSettingsSection.OnToolToggled += (toolName, enabled) => OnToolToggled?.Invoke(toolName, enabled);
|
||||
_toolSettingsSection.OnAllowThirdPartyChanged += value => OnAllowThirdPartyChanged?.Invoke(value);
|
||||
_toolSettingsSection.OnSecurityLevelChanged += value => OnSecurityLevelChanged?.Invoke(value);
|
||||
|
||||
_githubLinkRow = _root.Q<VisualElement>("github-link-row");
|
||||
_githubLinkLabel = _root.Q<Label>("github-link-label");
|
||||
_githubLinkIcon = _root.Q<Image>("github-link-icon");
|
||||
Debug.Assert(_githubLinkRow != null, "github-link-row must not be null");
|
||||
Debug.Assert(_githubLinkLabel != null, "github-link-label must not be null");
|
||||
Debug.Assert(_githubLinkIcon != null, "github-link-icon must not be null");
|
||||
_githubLinkRow.RegisterCallback<ClickEvent>(_ => HandleOpenGitHub());
|
||||
_githubLinkRow.RegisterCallback<MouseEnterEvent>(_ => HandleGitHubHoverChanged(true));
|
||||
_githubLinkRow.RegisterCallback<MouseLeaveEvent>(_ => HandleGitHubHoverChanged(false));
|
||||
InitializeGitHubIcon();
|
||||
}
|
||||
|
||||
private void ConfigureScrollView()
|
||||
{
|
||||
Debug.Assert(_mainScrollView != null, "mainScrollView must not be null");
|
||||
_mainScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto;
|
||||
_mainScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
|
||||
}
|
||||
|
||||
private void InitializeGitHubIcon()
|
||||
{
|
||||
string iconPath = $"{McpConstants.PackageAssetPath}/{GITHUB_ICON_RELATIVE_PATH}";
|
||||
Texture2D iconTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(iconPath);
|
||||
Debug.Assert(iconTexture != null, $"GitHub icon not found at {iconPath}");
|
||||
_githubLinkIcon.image = iconTexture;
|
||||
}
|
||||
|
||||
private static void HandleOpenGitHub()
|
||||
{
|
||||
Application.OpenURL(McpUIConstants.PROJECT_REPOSITORY_URL);
|
||||
}
|
||||
|
||||
private void HandleGitHubHoverChanged(bool isHovered)
|
||||
{
|
||||
ViewDataBinder.ToggleClass(_githubLinkRow, "mcp-github-link--hover", isHovered);
|
||||
ViewDataBinder.ToggleClass(_githubLinkLabel, "mcp-github-link__label--hover", isHovered);
|
||||
ViewDataBinder.ToggleClass(_githubLinkIcon, "mcp-github-link__icon--hover", isHovered);
|
||||
}
|
||||
|
||||
public void UpdateConnectedTools(ConnectedToolsData data)
|
||||
{
|
||||
_connectedToolsSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateEditorConfig(EditorConfigData data)
|
||||
{
|
||||
_editorConfigSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateToolSettings(ToolSettingsSectionData data)
|
||||
{
|
||||
_toolSettingsSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateSingleToolToggle(string toolName, bool enabled)
|
||||
{
|
||||
_toolSettingsSection?.UpdateSingleToggle(toolName, enabled);
|
||||
}
|
||||
|
||||
public void UpdateConnectionMode(ConnectionModeData data)
|
||||
{
|
||||
_connectionModeSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateConfigurationFoldout(bool show)
|
||||
{
|
||||
_connectionModeSection?.UpdateFoldout(show);
|
||||
}
|
||||
|
||||
public void UpdateCliSetup(CliSetupData data)
|
||||
{
|
||||
_cliSetupSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateSectionVisibility(ConnectionMode mode)
|
||||
{
|
||||
bool isMcp = mode == ConnectionMode.MCP;
|
||||
|
||||
ViewDataBinder.SetVisible(_mcpContent, isMcp);
|
||||
ViewDataBinder.SetVisible(_cliContent, !isMcp);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connectionModeSection = null;
|
||||
_cliSetupSection = null;
|
||||
_connectedToolsSection = null;
|
||||
_editorConfigSection = null;
|
||||
_toolSettingsSection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba059c58126bd44769bae90e953d8c3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user