chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖

This commit is contained in:
Liubing\LB
2026-09-28 18:14:59 +08:00
parent 0eb43b2b40
commit c4ae02def9
952 changed files with 70137 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
namespace io.github.hatayama.uLoopMCP
{
internal static class CliInstallRefreshPolicy
{
internal static bool ShouldRefreshSkillsAfterCliInstall(bool wasCliInstalledBeforeInstall)
{
return !wasCliInstalledBeforeInstall;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e244a03db91a449e0b0b0aed8f1a7abd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 984ecd7223ecf2e41b1f37a7662ef824
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 768573908011d4cdfac43e8e64888f7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 347cfd0ba9b6c45208b43ca1de434e6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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));
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab6c30c9ac75a40f09762d6481fec8ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d171c9fe3128441419f4a5b1dbc41ab2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10e986461c0be463d9d0b571b5af7661
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c03e148d650c4ce4aa37f6269d20777
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f8882470e8cb14e40a123cb1db1b8fa6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2c023a74a2ad48699e777e630eb7ce1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7f7b2604575c14efe8a88c27791da3fd
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 24cf07fd4b0ab49e090fda64d942f8bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48d305aead26c4120aca9cef9d4fcce7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 92fdf4f625a914ca186abb4fb130a4c7
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4ee9a3380d8747d8ad13bffd699d109
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ecd19e180f01a401a8249da9919bc8f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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&#10;Window &gt; Unity CLI Loop &gt; 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&#10;Window &gt; Unity CLI Loop &gt; 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>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 64588000e656f4a05be63d044cd1ad74
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bdcc9ecf5f9ec4e9684b2e1d0ed30976
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b85f09cd2bbf8472386a21e37979f9ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 57740db4675d74afb92c26bcc9d11913
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a0262ce83ca8f45d89b43d1dbd0ed551
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bfe94e1e1bb544deeb95197f97166dfc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6448f8d9a121c4da2ac15cf003171b63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f622365cf49674deb8b852dede89f11d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 927b0a3b6f73f4ef79fb88ad7228f9c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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()
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: edb525b005f3540e5bc7ab48fcbec256
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 429dc1f90707e4aa1aa2d9fa8c92d4c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee3977cf4f2304e6c940d5b1d343a3e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91dea545df5d649e384a2d4268db5822
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 718e4ecdb8013474282d23c964d9f98d
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba059c58126bd44769bae90e953d8c3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: