/*
┌──────────────────────────────────────────────────────────────────┐
│ 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