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,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: