/* ┌──────────────────────────────────────────────────────────────────┐ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ │ Copyright (c) 2025 Ivan Murzak │ │ Licensed under the Apache License, Version 2.0. │ │ See the LICENSE file in the project root for more information. │ └──────────────────────────────────────────────────────────────────┘ */ #nullable enable using System; using System.IO; using com.IvanMurzak.Unity.MCP.Editor.Utils; using R3; using UnityEngine; using UnityEngine.UIElements; using static com.IvanMurzak.McpPlugin.Common.Consts.MCP.Server; namespace com.IvanMurzak.Unity.MCP.Editor.UI { /// /// Base class for AI agent configurator UI components. /// Each AI agent has its own configurator that provides specific configuration instructions. /// public abstract class AiAgentConfigurator { #region Properties protected AiAgentConfig? _configStdio; protected AiAgentConfig? _configHttp; protected ConfigurationElements? _configElementStdio; protected ConfigurationElements? _configElementHttp; protected IDisposable? _subscriptionStdio; protected IDisposable? _subscriptionHttp; protected AlertPanel? _alertPanel; protected AlertPanel? _reconfigureAlertPanel; /// /// The display name of the AI agent. /// public abstract string AgentName { get; } /// /// The unique identifier for this agent (used for dropdown values and PlayerPrefs). /// public abstract string AgentId { get; } /// /// The download URL for the AI agent. /// public abstract string DownloadUrl { get; } /// /// The relative (or absolute) path where skill files should be generated for this agent. /// Return null if the agent does not support skills. /// public virtual string? SkillsPath => null; /// /// Whether this agent supports skill file generation. /// public bool SupportsSkills => SkillsPath != null; /// /// The tutorial URL for configuring the AI agent. /// public virtual string TutorialUrl => string.Empty; /// /// Gets the icon file name for this agent (e.g., "claude-64.png"). /// Return null if no icon should be displayed. /// protected abstract string? IconFileName { get; } protected VisualElement? Root { get; private set; } protected VisualElement? ContainerUnderHeader { get; private set; } protected VisualElement? ContainerAlert { get; private set; } protected VisualElement? ContainerHttp { get; private set; } protected VisualElement? ContainerStdio { get; private set; } protected VisualElement? ContainerSkills { get; private set; } protected Toggle? ToggleOptionHttp { get; private set; } protected Toggle? ToggleOptionStdio { get; private set; } /// /// Gets the icon paths for this agent. /// protected string[]? IconPaths => IconFileName != null ? EditorAssetLoader.GetEditorAssetPaths($"Editor/Gizmos/ai-agents/{IconFileName}") : null; /// /// Gets the agent configuration for the current platform. /// public AiAgentConfig ConfigStdio { get { if (_configStdio == null) { #if UNITY_EDITOR_WIN _configStdio = CreateConfigStdioWindows(); #else _configStdio = CreateConfigStdioMacLinux(); #endif ApplyStdioAuthorizationConfig(_configStdio); } return _configStdio; } } /// /// Gets the agent configuration for the current platform. /// public AiAgentConfig ConfigHttp { get { if (_configHttp == null) { #if UNITY_EDITOR_WIN _configHttp = CreateConfigHttpWindows(); #else _configHttp = CreateConfigHttpMacLinux(); #endif ApplyHttpAuthorizationConfig(_configHttp); } return _configHttp; } } /// /// Gets the Unity project root path (without /Assets suffix). /// protected static string ProjectRootPath { get { var dataPath = Application.dataPath.Replace('\\', Path.DirectorySeparatorChar) .Replace('/', Path.DirectorySeparatorChar); var suffix = $"{Path.DirectorySeparatorChar}Assets"; return dataPath.EndsWith(suffix) ? dataPath.Substring(0, dataPath.Length - suffix.Length) : dataPath; } } /// /// Returns a shortened display path relative to the project root. /// public static string ToDisplayPath(string fullPath) { var root = ProjectRootPath.Replace('\\', '/').TrimEnd('/') + '/'; var normalized = fullPath.Replace('\\', '/'); return normalized.StartsWith(root) ? normalized[root.Length..] : normalized; } /// /// Resolves a configurator's (which is project-relative for /// every built-in configurator — e.g. ".claude/skills") to an absolute filesystem /// path so the UI can show a clickable / copy-pasteable location. Mirrors the logic /// of but operates on /// the configurator-supplied value rather than the persisted one — this is important /// because the configurator's path can differ from the persisted one until the user /// enables skills for that agent. /// protected static string ResolveAbsoluteSkillsPath(string folder) { if (string.IsNullOrEmpty(folder)) return folder; return Path.IsPathRooted(folder) ? folder : Path.GetFullPath(Path.Combine(ProjectRootPath, folder)); } #endregion #region Abstract /// /// Creates the AI agent STDIO configuration for Windows platform. /// protected abstract AiAgentConfig CreateConfigStdioWindows(); /// /// Creates the AI agent STDIO configuration for Mac and Linux platforms. /// protected abstract AiAgentConfig CreateConfigStdioMacLinux(); /// /// Creates the AI agent HTTP configuration for Windows platform. /// protected abstract AiAgentConfig CreateConfigHttpWindows(); /// /// Creates the AI agent HTTP configuration for Mac and Linux platforms. /// protected abstract AiAgentConfig CreateConfigHttpMacLinux(); #endregion #region UI Templates protected Label TemplateLabelDescription(string? text = null) { var result = new UITemplate