chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57740db4675d74afb92c26bcc9d11913
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility for binding UI Toolkit elements to data without triggering change events.
|
||||
/// SetValueWithoutNotify prevents infinite loops when updating UI from model changes.
|
||||
/// </summary>
|
||||
public static class ViewDataBinder
|
||||
{
|
||||
public static void BindToggle(Toggle toggle, Func<bool> getter, Action<bool> onChanged)
|
||||
{
|
||||
toggle.SetValueWithoutNotify(getter());
|
||||
toggle.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
|
||||
}
|
||||
|
||||
public static void BindIntegerField(IntegerField field, Func<int> getter, Action<int> onChanged)
|
||||
{
|
||||
field.SetValueWithoutNotify(getter());
|
||||
field.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
|
||||
}
|
||||
|
||||
public static void BindEnumField<T>(EnumField field, Func<T> getter, Action<T> onChanged) where T : Enum
|
||||
{
|
||||
field.Init(getter());
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue is T newValue)
|
||||
{
|
||||
onChanged(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void BindFoldout(Foldout foldout, Func<bool> getter, Action<bool> onChanged)
|
||||
{
|
||||
foldout.SetValueWithoutNotify(getter());
|
||||
foldout.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
|
||||
}
|
||||
|
||||
public static void BindButton(Button button, Action onClick)
|
||||
{
|
||||
button.clicked += onClick;
|
||||
}
|
||||
|
||||
public static void BindLabel(Label label, Action<Label> onClick)
|
||||
{
|
||||
label.RegisterCallback<ClickEvent>(evt => onClick(label));
|
||||
}
|
||||
|
||||
public static void UpdateToggle(Toggle toggle, bool value)
|
||||
{
|
||||
toggle.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void UpdateIntegerField(IntegerField field, int value)
|
||||
{
|
||||
field.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void UpdateEnumField<T>(EnumField field, T value) where T : Enum
|
||||
{
|
||||
field.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void UpdateFoldout(Foldout foldout, bool value)
|
||||
{
|
||||
foldout.SetValueWithoutNotify(value);
|
||||
}
|
||||
|
||||
public static void SetVisible(VisualElement element, bool visible)
|
||||
{
|
||||
element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
public static void SetEnabled(VisualElement element, bool enabled)
|
||||
{
|
||||
element.SetEnabled(enabled);
|
||||
}
|
||||
|
||||
public static void AddModifierClass(VisualElement element, string baseClass, string modifier, bool condition)
|
||||
{
|
||||
string modifierClass = $"{baseClass}--{modifier}";
|
||||
if (condition)
|
||||
{
|
||||
element.AddToClassList(modifierClass);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.RemoveFromClassList(modifierClass);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ToggleClass(VisualElement element, string className, bool condition)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
element.AddToClassList(className);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.RemoveFromClassList(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0262ce83ca8f45d89b43d1dbd0ed551
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfe94e1e1bb544deeb95197f97166dfc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,333 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class CliSetupSection
|
||||
{
|
||||
private readonly VisualElement _cliStatusIcon;
|
||||
private readonly VisualElement _globalCliStatusRow;
|
||||
private readonly Label _cliStatusLabel;
|
||||
private readonly Button _refreshCliVersionButton;
|
||||
private readonly Button _installCliButton;
|
||||
private readonly VisualElement _globalCliSeparator;
|
||||
private readonly EnumField _skillsTargetField;
|
||||
private readonly Button _refreshSkillsStateButton;
|
||||
private readonly VisualElement _groupSkillsRow;
|
||||
private readonly Toggle _groupSkillsToggle;
|
||||
private readonly Label _groupSkillsLabel;
|
||||
private readonly VisualElement _projectCliVersionRow;
|
||||
private readonly Toggle _projectCliVersionToggle;
|
||||
private readonly Button _installSkillsButton;
|
||||
private readonly VisualElement _skillsSubsection;
|
||||
|
||||
private CliSetupData _lastData;
|
||||
private bool _isTargetFieldInitialized;
|
||||
|
||||
public event Action OnRefreshCliVersion;
|
||||
public event Action OnInstallCli;
|
||||
public event Action OnInstallSkills;
|
||||
public event Action OnRefreshSkillsState;
|
||||
public event Action<SkillsTarget> OnSkillsTargetChanged;
|
||||
public event Action<bool> OnGroupSkillsChanged;
|
||||
public event Action<bool> OnUseProjectCliVersionChanged;
|
||||
|
||||
public CliSetupSection(VisualElement root)
|
||||
{
|
||||
_cliStatusIcon = root.Q<VisualElement>("cli-status-icon");
|
||||
_globalCliStatusRow = root.Q<VisualElement>("global-cli-status-row");
|
||||
_cliStatusLabel = root.Q<Label>("cli-status-label");
|
||||
_refreshCliVersionButton = root.Q<Button>("refresh-cli-version-button");
|
||||
_installCliButton = root.Q<Button>("install-cli-button");
|
||||
_globalCliSeparator = root.Q<VisualElement>("global-cli-separator");
|
||||
_skillsTargetField = root.Q<EnumField>("skills-target-field");
|
||||
_refreshSkillsStateButton = root.Q<Button>("refresh-skills-state-button");
|
||||
_groupSkillsRow = root.Q<VisualElement>("group-skills-row");
|
||||
_groupSkillsToggle = root.Q<Toggle>("group-skills-toggle");
|
||||
_groupSkillsLabel = root.Q<Label>("group-skills-label");
|
||||
_projectCliVersionRow = root.Q<VisualElement>("project-cli-version-row");
|
||||
_projectCliVersionToggle = root.Q<Toggle>("project-cli-version-toggle");
|
||||
_installSkillsButton = root.Q<Button>("install-skills-button");
|
||||
_skillsSubsection = root.Q<VisualElement>("skills-subsection");
|
||||
}
|
||||
|
||||
public void SetupBindings()
|
||||
{
|
||||
_refreshCliVersionButton.clicked += () => OnRefreshCliVersion?.Invoke();
|
||||
_installCliButton.clicked += () => OnInstallCli?.Invoke();
|
||||
_installSkillsButton.clicked += () => OnInstallSkills?.Invoke();
|
||||
_refreshSkillsStateButton.clicked += () => OnRefreshSkillsState?.Invoke();
|
||||
_groupSkillsToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
OnGroupSkillsChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
_groupSkillsRow.RegisterCallback<ClickEvent>(HandleGroupSkillsRowClicked);
|
||||
_projectCliVersionToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
OnUseProjectCliVersionChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
_projectCliVersionRow.RegisterCallback<ClickEvent>(HandleProjectCliVersionRowClicked);
|
||||
}
|
||||
|
||||
public void Update(CliSetupData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
UpdateCliStatus(data);
|
||||
UpdateGlobalCliVisibility(data);
|
||||
UpdateRefreshButton(data);
|
||||
UpdateInstallCliButton(data);
|
||||
InitializeTargetFieldIfNeeded(data);
|
||||
UpdateRefreshSkillsButton(data);
|
||||
UpdateGroupSkillsToggle(data);
|
||||
UpdateProjectCliVersionToggle(data);
|
||||
UpdateSkillsSubsection(data);
|
||||
UpdateInstallSkillsButton(data);
|
||||
}
|
||||
|
||||
private void UpdateCliStatus(CliSetupData data)
|
||||
{
|
||||
if (data.IsChecking)
|
||||
{
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--installed", false);
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--not-installed", false);
|
||||
_cliStatusLabel.text = "uloop-cli: Checking...";
|
||||
return;
|
||||
}
|
||||
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--installed", data.IsCliInstalled);
|
||||
ViewDataBinder.ToggleClass(_cliStatusIcon, "mcp-cli-status-icon--not-installed", !data.IsCliInstalled);
|
||||
|
||||
if (data.IsCliInstalled && data.CliVersion != null)
|
||||
{
|
||||
_cliStatusLabel.text = $"uloop-cli: v{data.CliVersion}";
|
||||
return;
|
||||
}
|
||||
|
||||
_cliStatusLabel.text = "uloop-cli: Not installed";
|
||||
}
|
||||
|
||||
private void UpdateRefreshButton(CliSetupData data)
|
||||
{
|
||||
_refreshCliVersionButton.SetEnabled(!data.IsChecking);
|
||||
}
|
||||
|
||||
private void UpdateGlobalCliVisibility(CliSetupData data)
|
||||
{
|
||||
bool visible = !data.UseProjectCliVersion;
|
||||
ViewDataBinder.SetVisible(_globalCliStatusRow, visible);
|
||||
ViewDataBinder.SetVisible(_installCliButton, visible);
|
||||
ViewDataBinder.SetVisible(_globalCliSeparator, visible);
|
||||
}
|
||||
|
||||
private void UpdateInstallCliButton(CliSetupData data)
|
||||
{
|
||||
if (data.IsChecking)
|
||||
{
|
||||
SetCliButton("Checking...", false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.IsInstallingCli)
|
||||
{
|
||||
SetCliButton("Installing...", false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.IsCliInstalled)
|
||||
{
|
||||
SetCliButton("Install CLI", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.NeedsUpdate)
|
||||
{
|
||||
SetCliButton($"Update CLI (v{data.CliVersion} \u2192 v{data.PackageVersion})", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.NeedsDowngrade)
|
||||
{
|
||||
SetCliButton($"Downgrade CLI (v{data.CliVersion} \u2192 v{data.PackageVersion})", true);
|
||||
return;
|
||||
}
|
||||
|
||||
SetCliButton("Up to date", false);
|
||||
}
|
||||
|
||||
private void SetCliButton(string text, bool enabled)
|
||||
{
|
||||
_installCliButton.text = text;
|
||||
_installCliButton.SetEnabled(enabled);
|
||||
ViewDataBinder.ToggleClass(_installCliButton, "mcp-button--disabled", !enabled);
|
||||
}
|
||||
|
||||
private void InitializeTargetFieldIfNeeded(CliSetupData data)
|
||||
{
|
||||
if (!_isTargetFieldInitialized)
|
||||
{
|
||||
_skillsTargetField.Init(data.SelectedTarget);
|
||||
_skillsTargetField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue is SkillsTarget newValue)
|
||||
{
|
||||
OnSkillsTargetChanged?.Invoke(newValue);
|
||||
}
|
||||
});
|
||||
_isTargetFieldInitialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewDataBinder.UpdateEnumField(_skillsTargetField, data.SelectedTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRefreshSkillsButton(CliSetupData data)
|
||||
{
|
||||
bool canManageSkills = CanManageSkills(data);
|
||||
bool enabled = canManageSkills
|
||||
&& !data.IsChecking
|
||||
&& !data.IsInstallingCli
|
||||
&& !data.IsInstallingSkills;
|
||||
_refreshSkillsStateButton.SetEnabled(enabled);
|
||||
ViewDataBinder.ToggleClass(_refreshSkillsStateButton, "mcp-button--disabled", !enabled);
|
||||
}
|
||||
|
||||
private void UpdateGroupSkillsToggle(CliSetupData data)
|
||||
{
|
||||
ViewDataBinder.SetVisible(_groupSkillsRow, false);
|
||||
ViewDataBinder.UpdateToggle(_groupSkillsToggle, data.GroupSkillsUnderUnityCliLoop);
|
||||
_groupSkillsToggle.SetEnabled(CanManageSkills(data)
|
||||
&& !data.IsChecking
|
||||
&& !data.IsInstallingCli
|
||||
&& !data.IsInstallingSkills);
|
||||
}
|
||||
|
||||
private void UpdateProjectCliVersionToggle(CliSetupData data)
|
||||
{
|
||||
ViewDataBinder.UpdateToggle(_projectCliVersionToggle, data.UseProjectCliVersion);
|
||||
_projectCliVersionToggle.SetEnabled(!data.IsChecking
|
||||
&& !data.IsInstallingCli
|
||||
&& !data.IsInstallingSkills);
|
||||
}
|
||||
|
||||
private void UpdateSkillsSubsection(CliSetupData data)
|
||||
{
|
||||
bool enabled = CanManageSkills(data) && !data.IsChecking && !data.IsInstallingCli;
|
||||
_skillsSubsection.SetEnabled(enabled);
|
||||
}
|
||||
|
||||
private void UpdateInstallSkillsButton(CliSetupData data)
|
||||
{
|
||||
string label = GetInstallSkillsButtonText(
|
||||
CanManageSkills(data),
|
||||
data.IsInstallingSkills,
|
||||
data.SelectedTargetInstallState);
|
||||
bool enabled = IsInstallSkillsButtonEnabled(
|
||||
CanManageSkills(data),
|
||||
data.IsInstallingSkills,
|
||||
data.IsChecking || data.IsInstallingCli,
|
||||
data.SelectedTargetInstallState);
|
||||
SetSkillsButton(label, enabled);
|
||||
}
|
||||
|
||||
private static bool CanManageSkills(CliSetupData data)
|
||||
{
|
||||
return data.IsCliInstalled || data.UseProjectCliVersion;
|
||||
}
|
||||
|
||||
private void SetSkillsButton(string text, bool enabled)
|
||||
{
|
||||
_installSkillsButton.text = text;
|
||||
_installSkillsButton.SetEnabled(enabled);
|
||||
ViewDataBinder.ToggleClass(_installSkillsButton, "mcp-button--disabled", !enabled);
|
||||
}
|
||||
|
||||
internal static string GetInstallSkillsButtonText(
|
||||
bool isCliInstalled,
|
||||
bool isInstallingSkills,
|
||||
SkillInstallState installState)
|
||||
{
|
||||
if (isInstallingSkills)
|
||||
{
|
||||
return "Installing...";
|
||||
}
|
||||
|
||||
if (!isCliInstalled)
|
||||
{
|
||||
return "Install Skills";
|
||||
}
|
||||
|
||||
return installState switch
|
||||
{
|
||||
SkillInstallState.Checking => "Checking...",
|
||||
SkillInstallState.Installed => "Installed",
|
||||
SkillInstallState.Outdated => "Update Skills",
|
||||
_ => "Install Skills"
|
||||
};
|
||||
}
|
||||
|
||||
internal static bool IsInstallSkillsButtonEnabled(
|
||||
bool isCliInstalled,
|
||||
bool isInstallingSkills,
|
||||
bool isChecking,
|
||||
SkillInstallState installState)
|
||||
{
|
||||
if (!isCliInstalled || isInstallingSkills || isChecking)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return installState switch
|
||||
{
|
||||
SkillInstallState.Checking => false,
|
||||
SkillInstallState.Installed => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private void HandleGroupSkillsRowClicked(ClickEvent evt)
|
||||
{
|
||||
evt.StopPropagation();
|
||||
if (!_groupSkillsToggle.enabledSelf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.target is VisualElement targetElement && _groupSkillsToggle.Contains(targetElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool newValue = !_groupSkillsToggle.value;
|
||||
_groupSkillsToggle.SetValueWithoutNotify(newValue);
|
||||
OnGroupSkillsChanged?.Invoke(newValue);
|
||||
}
|
||||
|
||||
private void HandleProjectCliVersionRowClicked(ClickEvent evt)
|
||||
{
|
||||
evt.StopPropagation();
|
||||
if (!_projectCliVersionToggle.enabledSelf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.target is VisualElement targetElement && _projectCliVersionToggle.Contains(targetElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool newValue = !_projectCliVersionToggle.value;
|
||||
_projectCliVersionToggle.SetValueWithoutNotify(newValue);
|
||||
OnUseProjectCliVersionChanged?.Invoke(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6448f8d9a121c4da2ac15cf003171b63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class ConnectedToolsSection
|
||||
{
|
||||
private readonly VisualElement _sectionContainer;
|
||||
private readonly Foldout _foldout;
|
||||
private readonly VisualElement _clientsList;
|
||||
|
||||
private bool _hasLastValidData;
|
||||
private ConnectedToolsData _lastData;
|
||||
|
||||
public event Action<bool> OnFoldoutChanged;
|
||||
|
||||
public ConnectedToolsSection(VisualElement root)
|
||||
{
|
||||
_sectionContainer = root.Q<VisualElement>("connected-tools-section");
|
||||
_foldout = root.Q<Foldout>("connected-tools-foldout");
|
||||
_clientsList = root.Q<VisualElement>("clients-list");
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_foldout.RegisterValueChangedCallback(evt => OnFoldoutChanged?.Invoke(evt.newValue));
|
||||
}
|
||||
|
||||
public void Update(ConnectedToolsData data)
|
||||
{
|
||||
ViewDataBinder.SetVisible(_sectionContainer, data.ShowSection);
|
||||
|
||||
if (!data.ShowSection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.ShowReconnectingUI && _hasLastValidData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewDataBinder.UpdateFoldout(_foldout, data.ShowFoldout);
|
||||
UpdateClientsList(data);
|
||||
_lastData = data;
|
||||
}
|
||||
|
||||
private void UpdateClientsList(ConnectedToolsData data)
|
||||
{
|
||||
_clientsList.Clear();
|
||||
|
||||
if (data.Clients == null || data.Clients.Count == 0)
|
||||
{
|
||||
_hasLastValidData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
System.Collections.Generic.List<ConnectedClient> validClients = data.Clients
|
||||
.Where(client => IsValidClientName(client.ClientName))
|
||||
.ToList();
|
||||
|
||||
foreach (ConnectedClient client in validClients)
|
||||
{
|
||||
VisualElement clientItem = CreateClientItem(client);
|
||||
_clientsList.Add(clientItem);
|
||||
}
|
||||
|
||||
_hasLastValidData = validClients.Count > 0;
|
||||
}
|
||||
|
||||
private VisualElement CreateClientItem(ConnectedClient client)
|
||||
{
|
||||
VisualElement container = new VisualElement();
|
||||
container.AddToClassList("mcp-client");
|
||||
|
||||
Label iconLabel = new Label(McpUIConstants.CLIENT_ICON);
|
||||
iconLabel.AddToClassList("mcp-client__icon");
|
||||
container.Add(iconLabel);
|
||||
|
||||
Label nameLabel = new Label(client.ClientName);
|
||||
nameLabel.AddToClassList("mcp-client__name");
|
||||
container.Add(nameLabel);
|
||||
|
||||
Label portLabel = new Label($":{client.Port}");
|
||||
portLabel.AddToClassList("mcp-client__port");
|
||||
container.Add(portLabel);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private bool IsValidClientName(string clientName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(clientName) && clientName != McpConstants.UNKNOWN_CLIENT_NAME;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f622365cf49674deb8b852dede89f11d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class ConnectionModeSection
|
||||
{
|
||||
private readonly Foldout _foldout;
|
||||
private readonly Button _mcpTabButton;
|
||||
private readonly Button _cliTabButton;
|
||||
private ConnectionModeData _lastData;
|
||||
|
||||
public event Action<ConnectionMode> OnModeChanged;
|
||||
public event Action<bool> OnFoldoutChanged;
|
||||
|
||||
public ConnectionModeSection(VisualElement root)
|
||||
{
|
||||
_foldout = root.Q<Foldout>("configuration-foldout");
|
||||
_mcpTabButton = root.Q<Button>("mcp-tab-button");
|
||||
_cliTabButton = root.Q<Button>("cli-tab-button");
|
||||
|
||||
_mcpTabButton.clicked += () => OnModeChanged?.Invoke(ConnectionMode.MCP);
|
||||
_cliTabButton.clicked += () => OnModeChanged?.Invoke(ConnectionMode.CLI);
|
||||
_foldout.RegisterValueChangedCallback(evt => OnFoldoutChanged?.Invoke(evt.newValue));
|
||||
}
|
||||
|
||||
public void Update(ConnectionModeData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
bool isMcp = data.Mode == ConnectionMode.MCP;
|
||||
|
||||
ViewDataBinder.ToggleClass(_mcpTabButton, "mcp-tab-button--active", isMcp);
|
||||
ViewDataBinder.ToggleClass(_cliTabButton, "mcp-tab-button--active", !isMcp);
|
||||
}
|
||||
|
||||
public void UpdateFoldout(bool show)
|
||||
{
|
||||
ViewDataBinder.UpdateFoldout(_foldout, show);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 927b0a3b6f73f4ef79fb88ad7228f9c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section for LLM tool configuration (Cursor, Claude Code, etc).
|
||||
/// Handles editor type selection and MCP settings file generation.
|
||||
/// </summary>
|
||||
public class EditorConfigSection
|
||||
{
|
||||
private readonly EnumField _editorTypeField;
|
||||
private readonly VisualElement _repositoryRootRow;
|
||||
private readonly Toggle _repositoryRootToggle;
|
||||
private readonly Label _configErrorLabel;
|
||||
private readonly Button _configureButton;
|
||||
private readonly Button _deleteConfigButton;
|
||||
private readonly Button _openSettingsButton;
|
||||
|
||||
private EditorConfigData _lastData;
|
||||
private bool _isInitialized;
|
||||
|
||||
public event Action<McpEditorType> OnEditorTypeChanged;
|
||||
public event Action<bool> OnRepositoryRootChanged;
|
||||
public event Action OnConfigureClicked;
|
||||
public event Action OnDeleteConfigClicked;
|
||||
public event Action OnOpenSettingsClicked;
|
||||
|
||||
public EditorConfigSection(VisualElement root)
|
||||
{
|
||||
_editorTypeField = root.Q<EnumField>("editor-type-field");
|
||||
_repositoryRootRow = root.Q<VisualElement>("repository-root-row");
|
||||
_repositoryRootToggle = root.Q<Toggle>("repository-root-toggle");
|
||||
_configErrorLabel = root.Q<Label>("config-error-label");
|
||||
_configureButton = root.Q<Button>("configure-button");
|
||||
_deleteConfigButton = root.Q<Button>("delete-config-button");
|
||||
_openSettingsButton = root.Q<Button>("open-settings-button");
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_repositoryRootToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
// Foldout uses an internal Toggle that listens for ChangeEvent<bool>.
|
||||
// Without StopPropagation, this event bubbles up and collapses the Foldout.
|
||||
evt.StopPropagation();
|
||||
OnRepositoryRootChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
_configureButton.clicked += () => OnConfigureClicked?.Invoke();
|
||||
_deleteConfigButton.clicked += () => OnDeleteConfigClicked?.Invoke();
|
||||
_openSettingsButton.clicked += () => OnOpenSettingsClicked?.Invoke();
|
||||
}
|
||||
|
||||
public void Update(EditorConfigData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
InitializeEnumFieldIfNeeded(data);
|
||||
|
||||
UpdateRepositoryRootRow(data);
|
||||
UpdateConfigError(data);
|
||||
UpdateConfigureButton(data);
|
||||
UpdateDeleteButton(data);
|
||||
UpdateOpenSettingsButton(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EnumField.Init() can only be called once; subsequent calls reset the field and cause visual glitches.
|
||||
/// </summary>
|
||||
private void InitializeEnumFieldIfNeeded(EditorConfigData data)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_editorTypeField.Init(data.SelectedEditor);
|
||||
_editorTypeField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue is McpEditorType newValue)
|
||||
{
|
||||
OnEditorTypeChanged?.Invoke(newValue);
|
||||
}
|
||||
});
|
||||
_isInitialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewDataBinder.UpdateEnumField(_editorTypeField, data.SelectedEditor);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRepositoryRootRow(EditorConfigData data)
|
||||
{
|
||||
bool showRow = data.SupportsRepositoryRootToggle && data.ShowRepositoryRootToggle;
|
||||
ViewDataBinder.SetVisible(_repositoryRootRow, showRow);
|
||||
|
||||
if (showRow)
|
||||
{
|
||||
ViewDataBinder.UpdateToggle(_repositoryRootToggle, data.AddRepositoryRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateConfigError(EditorConfigData data)
|
||||
{
|
||||
bool hasError = !string.IsNullOrEmpty(data.ConfigurationError);
|
||||
ViewDataBinder.ToggleClass(_configErrorLabel, "mcp-error-message--visible", hasError);
|
||||
|
||||
if (hasError)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
_configErrorLabel.text = $"Error loading {editorName} configuration: {data.ConfigurationError}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateConfigureButton(EditorConfigData data)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
string buttonText;
|
||||
bool buttonEnabled = true;
|
||||
|
||||
if (data.IsChecking)
|
||||
{
|
||||
buttonText = $"Checking {editorName} Settings...";
|
||||
buttonEnabled = false;
|
||||
}
|
||||
else if (data.IsConfigured)
|
||||
{
|
||||
if (data.IsUpdateNeeded)
|
||||
{
|
||||
if (data.HasPortMismatch)
|
||||
{
|
||||
buttonText = data.IsServerRunning
|
||||
? $"Update {editorName} Settings\n(Port mismatch - Server: {data.CurrentPort})"
|
||||
: $"Update {editorName} Settings\n(Port mismatch)";
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText = data.IsServerRunning
|
||||
? $"Update {editorName} Settings\n(Port {data.CurrentPort})"
|
||||
: $"Update {editorName} Settings";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText = $"Settings Already Configured\n(Port {data.CurrentPort})";
|
||||
buttonEnabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText = $"Settings not found. \nConfigure {editorName}";
|
||||
}
|
||||
|
||||
_configureButton.text = buttonText;
|
||||
_configureButton.SetEnabled(buttonEnabled);
|
||||
|
||||
UpdateButtonStyle(data, buttonEnabled);
|
||||
}
|
||||
|
||||
private void UpdateButtonStyle(EditorConfigData data, bool buttonEnabled)
|
||||
{
|
||||
_configureButton.RemoveFromClassList("mcp-button--disabled");
|
||||
_configureButton.RemoveFromClassList("mcp-button--warning");
|
||||
|
||||
if (!buttonEnabled)
|
||||
{
|
||||
_configureButton.AddToClassList("mcp-button--disabled");
|
||||
}
|
||||
else if (!data.IsConfigured || data.HasPortMismatch)
|
||||
{
|
||||
_configureButton.AddToClassList("mcp-button--warning");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDeleteButton(EditorConfigData data)
|
||||
{
|
||||
ViewDataBinder.SetVisible(_deleteConfigButton, data.IsConfigured);
|
||||
|
||||
if (data.IsConfigured)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
_deleteConfigButton.text = $"Delete {editorName} Configuration";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOpenSettingsButton(EditorConfigData data)
|
||||
{
|
||||
string editorName = GetEditorDisplayName(data.SelectedEditor);
|
||||
_openSettingsButton.text = $"Open {editorName} Settings File";
|
||||
}
|
||||
|
||||
private string GetEditorDisplayName(McpEditorType editorType)
|
||||
{
|
||||
return editorType switch
|
||||
{
|
||||
McpEditorType.Cursor => "Cursor",
|
||||
McpEditorType.ClaudeCode => "Claude Code",
|
||||
McpEditorType.VSCode => "VSCode",
|
||||
McpEditorType.GeminiCLI => "Gemini CLI",
|
||||
McpEditorType.Codex => "Codex",
|
||||
McpEditorType.McpInspector => "MCP Inspector",
|
||||
_ => editorType.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edb525b005f3540e5bc7ab48fcbec256
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section for server start/stop controls and port configuration.
|
||||
/// Part of McpEditorWindowUI's component hierarchy.
|
||||
/// </summary>
|
||||
public class ServerControlsSection
|
||||
{
|
||||
private readonly IntegerField _portField;
|
||||
private readonly VisualElement _portWarningContainer;
|
||||
private readonly Label _portWarningLabel;
|
||||
private readonly Button _toggleServerButton;
|
||||
|
||||
private ServerControlsData _lastData;
|
||||
|
||||
public event Action OnToggleServer;
|
||||
public event Action<int> OnPortChanged;
|
||||
|
||||
public ServerControlsSection(VisualElement root)
|
||||
{
|
||||
_portField = root.Q<IntegerField>("port-field");
|
||||
_portWarningContainer = root.Q<VisualElement>("port-warning-container");
|
||||
_portWarningLabel = root.Q<Label>("port-warning-label");
|
||||
_toggleServerButton = root.Q<Button>("toggle-server-button");
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_portField.RegisterValueChangedCallback(evt => OnPortChanged?.Invoke(evt.newValue));
|
||||
_toggleServerButton.clicked += () => OnToggleServer?.Invoke();
|
||||
}
|
||||
|
||||
public void Update(ServerControlsData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
ViewDataBinder.UpdateIntegerField(_portField, data.CustomPort);
|
||||
_portField.SetEnabled(!data.IsServerRunning);
|
||||
|
||||
UpdatePortWarning(data);
|
||||
UpdateToggleButton(data);
|
||||
}
|
||||
|
||||
private void UpdatePortWarning(ServerControlsData data)
|
||||
{
|
||||
bool showWarning = data.HasPortWarning && !string.IsNullOrEmpty(data.PortWarningMessage);
|
||||
ViewDataBinder.ToggleClass(_portWarningContainer, "mcp-warning-container--visible", showWarning);
|
||||
|
||||
if (showWarning)
|
||||
{
|
||||
_portWarningLabel.text = data.PortWarningMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleButton(ServerControlsData data)
|
||||
{
|
||||
if (data.IsServerRunning)
|
||||
{
|
||||
_toggleServerButton.text = "Stop Server";
|
||||
_toggleServerButton.RemoveFromClassList("mcp-button--start");
|
||||
_toggleServerButton.AddToClassList("mcp-button--stop");
|
||||
}
|
||||
else
|
||||
{
|
||||
_toggleServerButton.text = "Start Server";
|
||||
_toggleServerButton.RemoveFromClassList("mcp-button--stop");
|
||||
_toggleServerButton.AddToClassList("mcp-button--start");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 429dc1f90707e4aa1aa2d9fa8c92d4c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section displaying server running status.
|
||||
/// Part of McpEditorWindowUI's component hierarchy.
|
||||
/// </summary>
|
||||
public class ServerStatusSection
|
||||
{
|
||||
private readonly Label _statusValueLabel;
|
||||
private readonly VisualElement _statusIndicator;
|
||||
private ServerStatusData _lastData;
|
||||
|
||||
public ServerStatusSection(VisualElement root)
|
||||
{
|
||||
_statusValueLabel = root.Q<Label>("status-value");
|
||||
_statusIndicator = root.Q<VisualElement>("server-status-indicator");
|
||||
}
|
||||
|
||||
public void Update(ServerStatusData data)
|
||||
{
|
||||
if (_lastData != null && _lastData.Equals(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastData = data;
|
||||
|
||||
_statusValueLabel.text = data.Status;
|
||||
|
||||
_statusValueLabel.RemoveFromClassList("mcp-server-status__text--running");
|
||||
_statusValueLabel.RemoveFromClassList("mcp-server-status__text--stopped");
|
||||
_statusIndicator.RemoveFromClassList("mcp-server-status__indicator--running");
|
||||
_statusIndicator.RemoveFromClassList("mcp-server-status__indicator--stopped");
|
||||
|
||||
if (data.IsRunning)
|
||||
{
|
||||
_statusValueLabel.AddToClassList("mcp-server-status__text--running");
|
||||
_statusIndicator.AddToClassList("mcp-server-status__indicator--running");
|
||||
}
|
||||
else
|
||||
{
|
||||
_statusValueLabel.AddToClassList("mcp-server-status__text--stopped");
|
||||
_statusIndicator.AddToClassList("mcp-server-status__indicator--stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee3977cf4f2304e6c940d5b1d343a3e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,562 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UI section for tool permissions and a virtualized per-tool enable list.
|
||||
/// The expensive list is allowed to stay unloaded while the foldout is collapsed.
|
||||
/// </summary>
|
||||
public class ToolSettingsSection
|
||||
{
|
||||
private const int ToolListRowHeight = 24;
|
||||
private const int InlineToolRowLimit = 40;
|
||||
|
||||
private readonly Foldout _foldout;
|
||||
private readonly Toggle _allowThirdPartyToggle;
|
||||
private readonly Label _allowThirdPartyLabel;
|
||||
private readonly Button _securityLevelRestrictedButton;
|
||||
private readonly Button _securityLevelFullAccessButton;
|
||||
private readonly Label _securityLevelDescription;
|
||||
private readonly VisualElement _toolSettingsInfoContainer;
|
||||
private readonly VisualElement _toolListContainer;
|
||||
private readonly Label _toolListStatusLabel;
|
||||
private readonly ListView _toolListView;
|
||||
private readonly List<ToolListRowData> _toolListRows = new();
|
||||
private readonly Dictionary<string, Toggle> _togglesByToolName = new();
|
||||
private bool _allowThirdPartyTools = true;
|
||||
private bool _isRegistryAvailable;
|
||||
private bool _isUnavailableStateShown;
|
||||
private bool _isLoadingStateShown;
|
||||
private string _layoutSignature = string.Empty;
|
||||
|
||||
public event Action<bool> OnFoldoutChanged;
|
||||
public event Action<string, bool> OnToolToggled;
|
||||
public event Action<bool> OnAllowThirdPartyChanged;
|
||||
public event Action<DynamicCodeSecurityLevel> OnSecurityLevelChanged;
|
||||
|
||||
public ToolSettingsSection(VisualElement root)
|
||||
{
|
||||
_foldout = root.Q<Foldout>("tool-settings-foldout");
|
||||
_allowThirdPartyToggle = root.Q<Toggle>("allow-third-party-toggle");
|
||||
_allowThirdPartyLabel = root.Q<Label>("allow-third-party-label");
|
||||
_securityLevelRestrictedButton = root.Q<Button>("security-level-restricted-button");
|
||||
_securityLevelFullAccessButton = root.Q<Button>("security-level-full-access-button");
|
||||
_securityLevelDescription = root.Q<Label>("security-level-description");
|
||||
_toolSettingsInfoContainer = root.Q<VisualElement>("tool-settings-info-container");
|
||||
_toolListContainer = root.Q<VisualElement>("tool-list-container");
|
||||
Debug.Assert(_toolListContainer != null, "tool-list-container must not be null");
|
||||
|
||||
_toolListStatusLabel = CreateToolListStatusLabel();
|
||||
_toolListView = CreateToolListView();
|
||||
_toolListContainer.Add(_toolListStatusLabel);
|
||||
_toolListContainer.Add(_toolListView);
|
||||
ClearToolList();
|
||||
|
||||
Label cliReferenceLink = root.Q<Label>("cli-reference-link");
|
||||
if (cliReferenceLink != null)
|
||||
{
|
||||
cliReferenceLink.RegisterCallback<ClickEvent>(_ => Application.OpenURL(McpUIConstants.CLI_COMMAND_REFERENCE_URL));
|
||||
}
|
||||
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
private void SetupBindings()
|
||||
{
|
||||
_foldout.RegisterValueChangedCallback(evt => OnFoldoutChanged?.Invoke(evt.newValue));
|
||||
|
||||
_allowThirdPartyToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
OnAllowThirdPartyChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
|
||||
_allowThirdPartyLabel.RegisterCallback<ClickEvent>(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
bool newValue = !_allowThirdPartyToggle.value;
|
||||
_allowThirdPartyToggle.SetValueWithoutNotify(newValue);
|
||||
OnAllowThirdPartyChanged?.Invoke(newValue);
|
||||
});
|
||||
|
||||
_securityLevelRestrictedButton.clicked += () => UpdateSecurityLevel(DynamicCodeSecurityLevel.Restricted);
|
||||
_securityLevelFullAccessButton.clicked += () => UpdateSecurityLevel(DynamicCodeSecurityLevel.FullAccess);
|
||||
}
|
||||
|
||||
public void Update(ToolSettingsSectionData data)
|
||||
{
|
||||
_allowThirdPartyTools = data.AllowThirdPartyTools;
|
||||
|
||||
ViewDataBinder.UpdateFoldout(_foldout, data.ShowToolSettings);
|
||||
ViewDataBinder.UpdateToggle(_allowThirdPartyToggle, data.AllowThirdPartyTools);
|
||||
UpdateSecurityLevelSelection(data.DynamicCodeSecurityLevel);
|
||||
UpdateSecurityLevelDescription(data.DynamicCodeSecurityLevel);
|
||||
|
||||
if (!data.ShowToolSettings)
|
||||
{
|
||||
ClearToolList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.HasToolListData)
|
||||
{
|
||||
UpdateDeferredState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.IsRegistryAvailable)
|
||||
{
|
||||
UpdateUnavailableState();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateToolList(data);
|
||||
}
|
||||
|
||||
public void UpdateSingleToggle(string toolName, bool enabled)
|
||||
{
|
||||
for (int i = 0; i < _toolListRows.Count; i++)
|
||||
{
|
||||
ToolListRowData row = _toolListRows[i];
|
||||
if (row.IsHeader || row.ToolName != toolName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
row.IsEnabled = enabled;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_togglesByToolName.TryGetValue(toolName, out Toggle toggle))
|
||||
{
|
||||
toggle.SetValueWithoutNotify(enabled);
|
||||
}
|
||||
|
||||
RefreshToolListView();
|
||||
}
|
||||
|
||||
private void UpdateDeferredState()
|
||||
{
|
||||
if (_toolListRows.Count > 0 || _isUnavailableStateShown)
|
||||
{
|
||||
UpdateThirdPartyGroupState(_allowThirdPartyTools);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateLoadingState();
|
||||
}
|
||||
|
||||
private void UpdateLoadingState()
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
_layoutSignature = string.Empty;
|
||||
|
||||
SetToolListStatus("Loading tools...");
|
||||
ViewDataBinder.SetVisible(_toolListView, false);
|
||||
SetToolSettingsInfoVisible(false);
|
||||
|
||||
_isRegistryAvailable = false;
|
||||
_isUnavailableStateShown = false;
|
||||
_isLoadingStateShown = true;
|
||||
}
|
||||
|
||||
private void UpdateUnavailableState()
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
_layoutSignature = string.Empty;
|
||||
|
||||
SetToolListStatus("Tool registry not yet initialized. Start the server first.");
|
||||
ViewDataBinder.SetVisible(_toolListView, false);
|
||||
SetToolSettingsInfoVisible(false);
|
||||
|
||||
_isRegistryAvailable = false;
|
||||
_isUnavailableStateShown = true;
|
||||
_isLoadingStateShown = false;
|
||||
}
|
||||
|
||||
private void ClearToolList()
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
_layoutSignature = string.Empty;
|
||||
|
||||
ViewDataBinder.SetVisible(_toolListStatusLabel, false);
|
||||
ViewDataBinder.SetVisible(_toolListView, false);
|
||||
SetToolSettingsInfoVisible(false);
|
||||
RefreshToolListView();
|
||||
|
||||
_isRegistryAvailable = false;
|
||||
_isUnavailableStateShown = false;
|
||||
_isLoadingStateShown = false;
|
||||
}
|
||||
|
||||
private void SetToolListStatus(string text)
|
||||
{
|
||||
_toolListStatusLabel.text = text;
|
||||
ViewDataBinder.SetVisible(_toolListStatusLabel, true);
|
||||
}
|
||||
|
||||
private void SetToolSettingsInfoVisible(bool visible)
|
||||
{
|
||||
if (_toolSettingsInfoContainer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewDataBinder.SetVisible(_toolSettingsInfoContainer, visible);
|
||||
}
|
||||
|
||||
private void UpdateToolList(ToolSettingsSectionData data)
|
||||
{
|
||||
string layoutSignature = CreateLayoutSignature(data);
|
||||
bool shouldRebuild = !_isRegistryAvailable
|
||||
|| _isUnavailableStateShown
|
||||
|| _isLoadingStateShown
|
||||
|| _layoutSignature != layoutSignature;
|
||||
|
||||
if (shouldRebuild)
|
||||
{
|
||||
Rebuild(data);
|
||||
_layoutSignature = layoutSignature;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateToggleStates(data.BuiltInTools);
|
||||
UpdateToggleStates(data.ThirdPartyTools);
|
||||
}
|
||||
|
||||
ViewDataBinder.SetVisible(_toolListStatusLabel, false);
|
||||
ViewDataBinder.SetVisible(_toolListView, true);
|
||||
SetToolSettingsInfoVisible(true);
|
||||
UpdateThirdPartyGroupState(data.AllowThirdPartyTools);
|
||||
|
||||
_isRegistryAvailable = true;
|
||||
_isUnavailableStateShown = false;
|
||||
_isLoadingStateShown = false;
|
||||
}
|
||||
|
||||
private void UpdateSecurityLevelDescription(DynamicCodeSecurityLevel currentLevel)
|
||||
{
|
||||
string description = currentLevel switch
|
||||
{
|
||||
DynamicCodeSecurityLevel.Restricted => "Dangerous APIs blocked (recommended)",
|
||||
DynamicCodeSecurityLevel.FullAccess => "All APIs available (use with caution)",
|
||||
_ => "Unknown level"
|
||||
};
|
||||
|
||||
_securityLevelDescription.text = description;
|
||||
_securityLevelDescription.RemoveFromClassList("mcp-security-level-description--warning");
|
||||
|
||||
if (currentLevel == DynamicCodeSecurityLevel.FullAccess)
|
||||
{
|
||||
_securityLevelDescription.AddToClassList("mcp-security-level-description--warning");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSecurityLevel(DynamicCodeSecurityLevel newLevel)
|
||||
{
|
||||
UpdateSecurityLevelSelection(newLevel);
|
||||
UpdateSecurityLevelDescription(newLevel);
|
||||
OnSecurityLevelChanged?.Invoke(newLevel);
|
||||
}
|
||||
|
||||
private void UpdateSecurityLevelSelection(DynamicCodeSecurityLevel currentLevel)
|
||||
{
|
||||
bool isRestricted = currentLevel == DynamicCodeSecurityLevel.Restricted;
|
||||
bool isFullAccess = currentLevel == DynamicCodeSecurityLevel.FullAccess;
|
||||
|
||||
ViewDataBinder.ToggleClass(_securityLevelRestrictedButton, "mcp-segmented-control__button--active", isRestricted);
|
||||
ViewDataBinder.ToggleClass(_securityLevelRestrictedButton, "mcp-segmented-control__button--warning-active", false);
|
||||
ViewDataBinder.ToggleClass(_securityLevelFullAccessButton, "mcp-segmented-control__button--active", isFullAccess);
|
||||
ViewDataBinder.ToggleClass(_securityLevelFullAccessButton, "mcp-segmented-control__button--warning-active", isFullAccess);
|
||||
}
|
||||
|
||||
private void Rebuild(ToolSettingsSectionData data)
|
||||
{
|
||||
_toolListRows.Clear();
|
||||
_togglesByToolName.Clear();
|
||||
|
||||
if (data.BuiltInTools.Length > 0)
|
||||
{
|
||||
_toolListRows.Add(ToolListRowData.CreateHeader("Built-in Tools", false));
|
||||
AddToolRows(data.BuiltInTools);
|
||||
}
|
||||
|
||||
if (data.ThirdPartyTools.Length > 0)
|
||||
{
|
||||
_toolListRows.Add(ToolListRowData.CreateHeader("Third Party Tools", true));
|
||||
AddToolRows(data.ThirdPartyTools);
|
||||
}
|
||||
|
||||
UpdateToolListHeight();
|
||||
RefreshToolListView();
|
||||
}
|
||||
|
||||
private void AddToolRows(IReadOnlyList<ToolToggleItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ToolToggleItem item = items[i];
|
||||
_toolListRows.Add(ToolListRowData.CreateTool(item));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleStates(IReadOnlyList<ToolToggleItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ToolToggleItem item = items[i];
|
||||
UpdateToggleState(item.ToolName, item.IsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleState(string toolName, bool isEnabled)
|
||||
{
|
||||
for (int i = 0; i < _toolListRows.Count; i++)
|
||||
{
|
||||
ToolListRowData row = _toolListRows[i];
|
||||
if (row.IsHeader || row.ToolName != toolName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
row.IsEnabled = isEnabled;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateLayoutSignature(ToolSettingsSectionData data)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
AppendGroupSignature(builder, data.BuiltInTools, "B");
|
||||
AppendGroupSignature(builder, data.ThirdPartyTools, "T");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendGroupSignature(StringBuilder builder, IReadOnlyList<ToolToggleItem> items, string group)
|
||||
{
|
||||
builder.Append(group);
|
||||
builder.Append(':');
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ToolToggleItem item = items[i];
|
||||
builder.Append(item.ToolName);
|
||||
builder.Append('|');
|
||||
}
|
||||
|
||||
builder.Append(';');
|
||||
}
|
||||
|
||||
private void UpdateThirdPartyGroupState(bool allowThirdPartyTools)
|
||||
{
|
||||
_allowThirdPartyTools = allowThirdPartyTools;
|
||||
RefreshToolListView();
|
||||
}
|
||||
|
||||
private static Label CreateToolListStatusLabel()
|
||||
{
|
||||
Label label = new Label();
|
||||
label.name = "tool-list-status-label";
|
||||
label.AddToClassList("mcp-tool-registry-unavailable");
|
||||
return label;
|
||||
}
|
||||
|
||||
private ListView CreateToolListView()
|
||||
{
|
||||
ListView listView = new ListView();
|
||||
listView.name = "tool-list-view";
|
||||
listView.AddToClassList("mcp-tool-list-view");
|
||||
listView.fixedItemHeight = ToolListRowHeight;
|
||||
listView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
|
||||
listView.selectionType = SelectionType.None;
|
||||
listView.itemsSource = _toolListRows;
|
||||
listView.makeItem = CreateToolListRowElement;
|
||||
listView.bindItem = BindToolListRowElement;
|
||||
listView.unbindItem = UnbindToolListRowElement;
|
||||
return listView;
|
||||
}
|
||||
|
||||
private static VisualElement CreateToolListRowElement()
|
||||
{
|
||||
VisualElement row = new VisualElement();
|
||||
row.AddToClassList("mcp-tool-toggle-row");
|
||||
row.AddToClassList("mcp-tool-list-row");
|
||||
|
||||
Toggle toggle = new Toggle();
|
||||
toggle.name = "tool-list-row-toggle";
|
||||
toggle.AddToClassList("mcp-tool-toggle-row__toggle");
|
||||
toggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
|
||||
if (row.userData is not ToolListRowData item || item.IsHeader)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.Owner?.OnToolToggled?.Invoke(item.ToolName, evt.newValue);
|
||||
});
|
||||
|
||||
Label label = new Label();
|
||||
label.name = "tool-list-row-label";
|
||||
label.AddToClassList("mcp-tool-toggle-row__label");
|
||||
label.RegisterCallback<ClickEvent>(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
|
||||
if (row.userData is not ToolListRowData item || item.IsHeader || !item.CanToggle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Toggle rowToggle = row.Q<Toggle>("tool-list-row-toggle");
|
||||
bool newValue = !rowToggle.value;
|
||||
rowToggle.SetValueWithoutNotify(newValue);
|
||||
item.Owner?.OnToolToggled?.Invoke(item.ToolName, newValue);
|
||||
});
|
||||
|
||||
row.Add(toggle);
|
||||
row.Add(label);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void BindToolListRowElement(VisualElement row, int index)
|
||||
{
|
||||
Debug.Assert(index >= 0 && index < _toolListRows.Count, "tool list index must be valid");
|
||||
|
||||
ToolListRowData item = _toolListRows[index];
|
||||
item.Owner = this;
|
||||
item.CanToggle = !item.IsThirdParty || _allowThirdPartyTools;
|
||||
row.userData = item;
|
||||
|
||||
Toggle toggle = row.Q<Toggle>("tool-list-row-toggle");
|
||||
Label label = row.Q<Label>("tool-list-row-label");
|
||||
Debug.Assert(toggle != null, "tool-list-row-toggle must not be null");
|
||||
Debug.Assert(label != null, "tool-list-row-label must not be null");
|
||||
|
||||
ResetRowClasses(row, label);
|
||||
|
||||
if (item.IsHeader)
|
||||
{
|
||||
BindHeaderRow(row, toggle, label, item);
|
||||
return;
|
||||
}
|
||||
|
||||
BindToolRow(row, toggle, label, item);
|
||||
}
|
||||
|
||||
private void BindHeaderRow(VisualElement row, Toggle toggle, Label label, ToolListRowData item)
|
||||
{
|
||||
ViewDataBinder.SetVisible(toggle, false);
|
||||
label.text = item.Label;
|
||||
label.tooltip = string.Empty;
|
||||
row.SetEnabled(true);
|
||||
row.AddToClassList("mcp-tool-list-row--header");
|
||||
label.AddToClassList("mcp-tool-group-header");
|
||||
ViewDataBinder.ToggleClass(row, "mcp-tool-list-row--disabled", item.IsThirdParty && !_allowThirdPartyTools);
|
||||
}
|
||||
|
||||
private void BindToolRow(VisualElement row, Toggle toggle, Label label, ToolListRowData item)
|
||||
{
|
||||
ViewDataBinder.SetVisible(toggle, true);
|
||||
toggle.SetValueWithoutNotify(item.IsEnabled);
|
||||
label.text = item.ToolName;
|
||||
label.tooltip = item.Description;
|
||||
|
||||
row.SetEnabled(item.CanToggle);
|
||||
ViewDataBinder.ToggleClass(row, "mcp-tool-list-row--disabled", !item.CanToggle);
|
||||
_togglesByToolName[item.ToolName] = toggle;
|
||||
}
|
||||
|
||||
private static void ResetRowClasses(VisualElement row, Label label)
|
||||
{
|
||||
row.RemoveFromClassList("mcp-tool-list-row--header");
|
||||
row.RemoveFromClassList("mcp-tool-list-row--disabled");
|
||||
label.RemoveFromClassList("mcp-tool-group-header");
|
||||
}
|
||||
|
||||
private void UnbindToolListRowElement(VisualElement row, int index)
|
||||
{
|
||||
if (row.userData is ToolListRowData item && !item.IsHeader)
|
||||
{
|
||||
_togglesByToolName.Remove(item.ToolName);
|
||||
}
|
||||
|
||||
row.userData = null;
|
||||
}
|
||||
|
||||
private void RefreshToolListView()
|
||||
{
|
||||
_togglesByToolName.Clear();
|
||||
_toolListView.RefreshItems();
|
||||
}
|
||||
|
||||
private void UpdateToolListHeight()
|
||||
{
|
||||
int visibleRows = Math.Min(_toolListRows.Count, InlineToolRowLimit);
|
||||
if (visibleRows <= 0)
|
||||
{
|
||||
visibleRows = 1;
|
||||
}
|
||||
|
||||
_toolListView.style.height = (visibleRows * ToolListRowHeight) + 2;
|
||||
}
|
||||
|
||||
private sealed class ToolListRowData
|
||||
{
|
||||
public readonly bool IsHeader;
|
||||
public readonly string ToolName;
|
||||
public readonly string Label;
|
||||
public readonly string Description;
|
||||
public readonly bool IsThirdParty;
|
||||
public bool IsEnabled;
|
||||
public bool CanToggle = true;
|
||||
public ToolSettingsSection Owner;
|
||||
|
||||
private ToolListRowData(
|
||||
bool isHeader,
|
||||
string toolName,
|
||||
string label,
|
||||
string description,
|
||||
bool isEnabled,
|
||||
bool isThirdParty)
|
||||
{
|
||||
IsHeader = isHeader;
|
||||
ToolName = toolName;
|
||||
Label = label;
|
||||
Description = description;
|
||||
IsEnabled = isEnabled;
|
||||
IsThirdParty = isThirdParty;
|
||||
}
|
||||
|
||||
public static ToolListRowData CreateHeader(string label, bool isThirdParty)
|
||||
{
|
||||
return new ToolListRowData(
|
||||
true,
|
||||
string.Empty,
|
||||
label,
|
||||
string.Empty,
|
||||
true,
|
||||
isThirdParty);
|
||||
}
|
||||
|
||||
public static ToolListRowData CreateTool(ToolToggleItem item)
|
||||
{
|
||||
return new ToolListRowData(
|
||||
false,
|
||||
item.ToolName,
|
||||
item.ToolName,
|
||||
item.Description,
|
||||
item.IsEnabled,
|
||||
item.IsThirdParty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91dea545df5d649e384a2d4268db5822
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,748 @@
|
||||
/* ===========================================
|
||||
McpEditorWindow UI Toolkit Stylesheet
|
||||
Using BEM naming convention and Unity theme variables
|
||||
=========================================== */
|
||||
|
||||
:root {
|
||||
--mcp-color-success: #4caf50;
|
||||
--mcp-color-danger: #ef5350;
|
||||
--mcp-color-warning: #ffd200;
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.mcp-window-root {
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.mcp-main-container {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mcp-main-container--debug {
|
||||
background-color: rgba(128, 0, 128, 0.15);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Section
|
||||
=========================================== */
|
||||
.mcp-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-section--box {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Server Status (prominent status row)
|
||||
=========================================== */
|
||||
.mcp-server-status {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator--running {
|
||||
background-color: var(--mcp-color-success);
|
||||
}
|
||||
|
||||
.mcp-server-status__indicator--stopped {
|
||||
background-color: var(--mcp-color-danger);
|
||||
}
|
||||
|
||||
.mcp-server-status__text {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.mcp-server-status__text--running {
|
||||
color: var(--mcp-color-success);
|
||||
border-color: var(--mcp-color-success);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.mcp-server-status__text--stopped {
|
||||
color: var(--mcp-color-danger);
|
||||
border-color: var(--mcp-color-danger);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.mcp-server-status__spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-server-status__port-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-server-status__port-field {
|
||||
width: 60px;
|
||||
-unity-text-align: middle-right;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Server Controls (compact row)
|
||||
=========================================== */
|
||||
.mcp-server-controls {
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.mcp-server-controls .mcp-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Warning Container
|
||||
=========================================== */
|
||||
.mcp-warning-container {
|
||||
display: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-warning-container--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-warning-label {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Button
|
||||
=========================================== */
|
||||
.mcp-button {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-button--compact {
|
||||
height: 22px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mcp-button--start {
|
||||
background-color: #2e7d32;
|
||||
border-color: var(--mcp-color-success);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mcp-button--start:hover {
|
||||
background-color: #388e3c;
|
||||
}
|
||||
|
||||
.mcp-button--stop {
|
||||
background-color: #4a4a4a;
|
||||
border-color: #666666;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.mcp-button--stop:hover {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
.mcp-button--warning {
|
||||
background-color: #665522;
|
||||
border-color: #aa9944;
|
||||
}
|
||||
|
||||
.mcp-button--warning:hover {
|
||||
background-color: #887744;
|
||||
}
|
||||
|
||||
.mcp-button--configure {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#install-cli-button {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mcp-button--delete {
|
||||
margin-top: 12px;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.mcp-button--delete:hover {
|
||||
color: #ff8a8a;
|
||||
}
|
||||
|
||||
.mcp-button--disabled {
|
||||
background-color: #4a4a4a;
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.5);
|
||||
color: #999999;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Toggle Row
|
||||
=========================================== */
|
||||
.mcp-toggle-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-toggle-row__toggle {
|
||||
width: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-toggle-row__label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-toggle-row__label--danger {
|
||||
color: var(--mcp-color-warning);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Target Row (Editor Selection)
|
||||
=========================================== */
|
||||
.mcp-target-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mcp-target-row__label {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcp-target-row__field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Foldout
|
||||
=========================================== */
|
||||
.mcp-foldout {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
#tool-settings-foldout > .unity-foldout__content {
|
||||
margin-top: 2px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Client (Connected Tools Item)
|
||||
=========================================== */
|
||||
.mcp-clients-list {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mcp-client {
|
||||
background-color: rgba(76, 76, 76, 0.5);
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.5);
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
margin-bottom: 6px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mcp-client__icon {
|
||||
color: var(--mcp-color-success);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-client__name {
|
||||
-unity-font-style: bold;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-client__port {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Messages
|
||||
=========================================== */
|
||||
.mcp-info-message {
|
||||
display: none;
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mcp-info-message--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-warning-message {
|
||||
display: none;
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mcp-warning-message--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-error-message {
|
||||
display: none;
|
||||
background-color: var(--unity-colors-error-background, #440000);
|
||||
border-color: var(--unity-colors-error-border, #aa0000);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
white-space: normal;
|
||||
color: var(--unity-colors-error-text, #ff4444);
|
||||
}
|
||||
|
||||
.mcp-error-message--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Tool Settings
|
||||
=========================================== */
|
||||
.mcp-tool-settings-info {
|
||||
white-space: normal;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-note {
|
||||
font-size: 10px;
|
||||
color: #888888;
|
||||
white-space: normal;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-link {
|
||||
font-size: 10px;
|
||||
color: #4C7EF3;
|
||||
white-space: normal;
|
||||
margin-bottom: 8px;
|
||||
cursor: link;
|
||||
-unity-font-style: italic;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-link:hover {
|
||||
color: #6B9AFF;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-content {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#tool-settings-info-container {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-card {
|
||||
border-width: 1px;
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
padding: 0px 8px 8px 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mcp-tool-settings-card__header {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mcp-tool-group-header {
|
||||
-unity-font-style: bold;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-tool-group--disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.mcp-permission-item {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0 0 14px;
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.mcp-permission-item--first {
|
||||
margin-top: 0;
|
||||
padding-top: 4px;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.mcp-permission-item__title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
color: var(--mcp-color-warning);
|
||||
}
|
||||
|
||||
.mcp-permission-item__title--dynamic-code {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mcp-permission-item__control-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mcp-permission-item__control-label {
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.mcp-permission-item__toggle-row {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mcp-permission-item__assistive-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mcp-segmented-control {
|
||||
flex-direction: row;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
background-color: rgba(48, 48, 48, 0.6);
|
||||
border-radius: 3px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button {
|
||||
flex-grow: 1;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
-unity-font-style: normal;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button:hover {
|
||||
background-color: rgba(46, 90, 136, 0.3);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
#security-level-full-access-button:hover {
|
||||
background-color: rgba(122, 39, 39, 0.35);
|
||||
color: #ffe3e3;
|
||||
border-width: 1px;
|
||||
border-color: rgba(178, 76, 76, 0.55);
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button--active {
|
||||
background-color: rgba(46, 90, 136, 0.8);
|
||||
color: #ffffff;
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
.mcp-segmented-control__button--warning-active {
|
||||
background-color: rgba(122, 39, 39, 0.85);
|
||||
color: #ffd7d7;
|
||||
border-width: 1px;
|
||||
border-color: rgba(178, 76, 76, 0.7);
|
||||
}
|
||||
|
||||
.mcp-tool-toggle-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 2px 4px 2px 14px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.mcp-tool-toggle-row__toggle {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcp-tool-toggle-row__label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-tool-list-container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.mcp-tool-list-view {
|
||||
margin-top: 4px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mcp-tool-list-row {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.mcp-tool-list-row--header {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mcp-tool-list-row--disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.mcp-tool-registry-unavailable {
|
||||
color: #888888;
|
||||
-unity-font-style: italic;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Security
|
||||
=========================================== */
|
||||
.mcp-security-warning {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
white-space: normal;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.mcp-section-header {
|
||||
-unity-font-style: bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-section-header--danger {
|
||||
color: var(--mcp-color-warning);
|
||||
}
|
||||
|
||||
.mcp-security-level-field {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-security-level-description {
|
||||
background-color: var(--unity-colors-helpbox-background);
|
||||
border-color: var(--unity-colors-helpbox-border);
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
margin-top: 6px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mcp-security-level-description--warning {
|
||||
background-color: #5c4a00;
|
||||
border-color: #7a6400;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: Tab Toggle (segment control)
|
||||
=========================================== */
|
||||
.mcp-tab-toggle {
|
||||
flex-direction: row;
|
||||
background-color: rgba(48, 48, 48, 0.6);
|
||||
border-radius: 3px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.2);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.mcp-tab-button {
|
||||
flex-grow: 1;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
-unity-font-style: normal;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mcp-tab-button:hover {
|
||||
background-color: rgba(46, 90, 136, 0.3);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
.mcp-tab-button--active {
|
||||
background-color: rgba(46, 90, 136, 0.8);
|
||||
color: #ffffff;
|
||||
border-width: 1px;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
.mcp-tab-button--active:hover {
|
||||
background-color: rgba(46, 90, 136, 0.8);
|
||||
color: #ffffff;
|
||||
border-color: rgba(70, 130, 200, 0.4);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: CLI Setup
|
||||
=========================================== */
|
||||
.mcp-cli-subsection-header {
|
||||
-unity-font-style: bold;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mcp-cli-separator {
|
||||
height: 1px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 6px;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.mcp-cli-status-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#cli-status-label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mcp-cli-status-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mcp-cli-status-icon--installed {
|
||||
background-color: var(--mcp-color-success);
|
||||
}
|
||||
|
||||
.mcp-cli-status-icon--not-installed {
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
|
||||
.mcp-button--refresh {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
margin: 0 3px 0 4px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
-unity-text-align: middle-center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Block: GitHub Link
|
||||
=========================================== */
|
||||
.mcp-github-link {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding-top: 4px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 4px;
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
margin-right: 6px;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.mcp-github-link__label {
|
||||
font-size: 11px;
|
||||
color: #c3cad3;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mcp-github-link__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.78;
|
||||
-unity-background-scale-mode: scale-to-fit;
|
||||
}
|
||||
|
||||
.mcp-github-link--hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mcp-github-link__label--hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mcp-github-link__icon--hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Utility: Hidden
|
||||
=========================================== */
|
||||
.mcp-hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b160f81152d6c4270bb39f5ba88ba7ed
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
<ui:VisualElement name="window-root" class="mcp-window-root">
|
||||
<ui:ScrollView name="main-scroll-view" class="mcp-main-container">
|
||||
<!-- Configuration Section -->
|
||||
<ui:VisualElement name="configuration-section" class="mcp-section mcp-section--box">
|
||||
<ui:Foldout name="configuration-foldout" text="Configuration" class="mcp-foldout">
|
||||
<ui:VisualElement class="mcp-tab-toggle">
|
||||
<ui:Button name="cli-tab-button" text="CLI" class="mcp-tab-button mcp-tab-button--active" />
|
||||
<ui:Button name="mcp-tab-button" text="MCP" class="mcp-tab-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- CLI Content -->
|
||||
<ui:VisualElement name="cli-content">
|
||||
<ui:VisualElement name="project-cli-version-row" class="mcp-toggle-row">
|
||||
<ui:Toggle name="project-cli-version-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label
|
||||
name="project-cli-version-label"
|
||||
text="Use project CLI version"
|
||||
tooltip="Run installed skills through the Unity package version of uloop-cli."
|
||||
class="mcp-toggle-row__label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="global-cli-status-row" class="mcp-cli-status-row">
|
||||
<ui:VisualElement name="cli-status-icon" class="mcp-cli-status-icon mcp-cli-status-icon--not-installed" />
|
||||
<ui:Label name="cli-status-label" text="uloop-cli: Checking..." />
|
||||
<ui:Button name="refresh-cli-version-button" text="↻" class="mcp-button--refresh" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="install-cli-button" text="Install CLI" class="mcp-button mcp-button--configure" />
|
||||
<ui:VisualElement name="global-cli-separator" class="mcp-cli-separator" />
|
||||
|
||||
<ui:VisualElement name="skills-subsection">
|
||||
<ui:Label text="Skills" class="mcp-cli-subsection-header" />
|
||||
<ui:VisualElement name="group-skills-row" class="mcp-toggle-row">
|
||||
<ui:Toggle name="group-skills-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label
|
||||
name="group-skills-label"
|
||||
text="Group in subfolder"
|
||||
class="mcp-toggle-row__label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="mcp-target-row">
|
||||
<ui:Label text="Target:" class="mcp-target-row__label" />
|
||||
<uie:EnumField name="skills-target-field" class="mcp-target-row__field" />
|
||||
<ui:Button name="refresh-skills-state-button" text="↻" class="mcp-button--refresh" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="install-skills-button" text="Install Skills" class="mcp-button mcp-button--configure" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- MCP Content -->
|
||||
<ui:VisualElement name="mcp-content">
|
||||
<ui:VisualElement class="mcp-warning-container mcp-warning-container--visible">
|
||||
<ui:Label
|
||||
text="This MCP configuration is already deprecated. Please use the CLI instead. The MCP feature will be removed in a future release."
|
||||
class="mcp-warning-label" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="mcp-target-row">
|
||||
<ui:Label text="Target:" class="mcp-target-row__label" />
|
||||
<uie:EnumField name="editor-type-field" class="mcp-target-row__field" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="repository-root-row" class="mcp-toggle-row">
|
||||
<ui:Toggle name="repository-root-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label text="Use Repository Root" class="mcp-toggle-row__label" tooltip="When enabled, generates configuration file at the Git repository root" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:Label name="config-error-label" class="mcp-error-message" />
|
||||
|
||||
<ui:Button name="configure-button" text="Configure" class="mcp-button mcp-button--configure" />
|
||||
<ui:Button name="open-settings-button" text="Open Settings File" class="mcp-button" />
|
||||
<ui:Button name="delete-config-button" text="Delete Configuration" class="mcp-button mcp-button--delete" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Connected LLM Tools Section -->
|
||||
<ui:VisualElement name="connected-tools-section" class="mcp-section mcp-section--box">
|
||||
<ui:Foldout name="connected-tools-foldout" text="Connected LLM Tools (via MCP)" class="mcp-foldout">
|
||||
<ui:VisualElement name="connected-tools-content">
|
||||
<ui:VisualElement name="clients-list" class="mcp-clients-list" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Tool Settings Section -->
|
||||
<ui:VisualElement name="tool-settings-section" class="mcp-section mcp-section--box">
|
||||
<ui:Foldout name="tool-settings-foldout" text="Tool Settings" class="mcp-foldout">
|
||||
<ui:VisualElement name="tool-settings-content" class="mcp-tool-settings-content">
|
||||
<ui:VisualElement class="mcp-tool-settings-card">
|
||||
<ui:Label text="Permissions" class="mcp-section-header mcp-tool-settings-card__header" />
|
||||
<ui:VisualElement class="mcp-permission-item mcp-permission-item--first">
|
||||
<ui:Label text="Allow Third Party Tools" class="mcp-permission-item__title mcp-section-header--danger" />
|
||||
<ui:VisualElement class="mcp-permission-item__control-row">
|
||||
<ui:VisualElement class="mcp-toggle-row mcp-permission-item__toggle-row">
|
||||
<ui:Toggle name="allow-third-party-toggle" class="mcp-toggle-row__toggle" />
|
||||
<ui:Label text="Enable" class="mcp-permission-item__control-label" />
|
||||
<ui:Label name="allow-third-party-label" text="Allow Third Party Tools" class="mcp-toggle-row__label mcp-toggle-row__label--danger mcp-permission-item__assistive-label" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="mcp-permission-item">
|
||||
<ui:Label text="execute-dynamic-code" class="mcp-permission-item__title mcp-permission-item__title--dynamic-code mcp-section-header--danger" />
|
||||
<ui:Label text="Security Level" class="mcp-permission-item__control-label" />
|
||||
<ui:VisualElement class="mcp-segmented-control">
|
||||
<ui:Button name="security-level-restricted-button" text="Restricted" class="mcp-segmented-control__button" />
|
||||
<ui:Button name="security-level-full-access-button" text="Full Access" class="mcp-segmented-control__button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="security-level-description" class="mcp-security-level-description" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="tool-settings-info-container">
|
||||
<ui:Label text="Enable or disable individual tools. Disabled tools are completely hidden from AI agents, eliminating unnecessary context window consumption." class="mcp-tool-settings-info" />
|
||||
<ui:Label text="Changes are notified to connected MCP clients immediately." class="mcp-tool-settings-note" />
|
||||
<ui:Label name="cli-reference-link" text="See CLI Command Reference for uloop-cli usage." class="mcp-tool-settings-link" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="tool-list-container" class="mcp-tool-list-container" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
</ui:ScrollView>
|
||||
|
||||
<ui:VisualElement name="github-link-row" class="mcp-github-link">
|
||||
<ui:Label name="github-link-label" text="Star on GitHub" class="mcp-github-link__label" />
|
||||
<ui:Image name="github-link-icon" class="mcp-github-link__icon" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 718e4ecdb8013474282d23c964d9f98d
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// View layer for McpEditorWindow in MVP architecture.
|
||||
/// Owns UI sections and forwards user interactions to presenter via events.
|
||||
/// Related: McpEditorWindow (presenter), McpEditorModel (model)
|
||||
/// </summary>
|
||||
public class McpEditorWindowUI : IDisposable
|
||||
{
|
||||
private const string UXML_RELATIVE_PATH = "Editor/UI/UIToolkit/McpEditorWindow.uxml";
|
||||
private const string USS_RELATIVE_PATH = "Editor/UI/UIToolkit/McpEditorWindow.uss";
|
||||
private const string GITHUB_ICON_RELATIVE_PATH = "Editor/UI/Setup/GitHub_Invertocat_White.png";
|
||||
|
||||
private readonly VisualElement _root;
|
||||
|
||||
private ConnectionModeSection _connectionModeSection;
|
||||
private CliSetupSection _cliSetupSection;
|
||||
private ConnectedToolsSection _connectedToolsSection;
|
||||
private EditorConfigSection _editorConfigSection;
|
||||
private ToolSettingsSection _toolSettingsSection;
|
||||
|
||||
private VisualElement _cliContent;
|
||||
private VisualElement _mcpContent;
|
||||
private ScrollView _mainScrollView;
|
||||
private VisualElement _githubLinkRow;
|
||||
private Label _githubLinkLabel;
|
||||
private Image _githubLinkIcon;
|
||||
|
||||
public event Action<ConnectionMode> OnConnectionModeChanged;
|
||||
public event Action OnRefreshCliVersion;
|
||||
public event Action OnInstallCli;
|
||||
public event Action OnInstallSkills;
|
||||
public event Action OnRefreshSkillsState;
|
||||
public event Action<SkillsTarget> OnSkillsTargetChanged;
|
||||
public event Action<bool> OnGroupSkillsChanged;
|
||||
public event Action<bool> OnUseProjectCliVersionChanged;
|
||||
public event Action<bool> OnConfigurationFoldoutChanged;
|
||||
public event Action<bool> OnConnectedToolsFoldoutChanged;
|
||||
public event Action<McpEditorType> OnEditorTypeChanged;
|
||||
public event Action<bool> OnRepositoryRootChanged;
|
||||
public event Action OnConfigureClicked;
|
||||
public event Action OnDeleteConfigClicked;
|
||||
public event Action OnOpenSettingsClicked;
|
||||
public event Action<bool> OnToolSettingsFoldoutChanged;
|
||||
public event Action<string, bool> OnToolToggled;
|
||||
public event Action<bool> OnAllowThirdPartyChanged;
|
||||
public event Action<DynamicCodeSecurityLevel> OnSecurityLevelChanged;
|
||||
|
||||
public McpEditorWindowUI(VisualElement root)
|
||||
{
|
||||
_root = root;
|
||||
LoadLayout();
|
||||
ApplyDebugStyle();
|
||||
InitializeSections();
|
||||
}
|
||||
|
||||
private void ApplyDebugStyle()
|
||||
{
|
||||
#if ULOOPMCP_DEBUG
|
||||
VisualElement mainContainer = _root.Q<VisualElement>("main-scroll-view");
|
||||
mainContainer?.AddToClassList("mcp-main-container--debug");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void LoadLayout()
|
||||
{
|
||||
string uxmlPath = $"{McpConstants.PackageAssetPath}/{UXML_RELATIVE_PATH}";
|
||||
string ussPath = $"{McpConstants.PackageAssetPath}/{USS_RELATIVE_PATH}";
|
||||
|
||||
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
|
||||
if (visualTree == null)
|
||||
{
|
||||
Debug.LogError($"Failed to load UXML from: {uxmlPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
visualTree.CloneTree(_root);
|
||||
|
||||
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
|
||||
if (styleSheet == null)
|
||||
{
|
||||
Debug.LogError($"Failed to load USS from: {ussPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
_root.styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
private void InitializeSections()
|
||||
{
|
||||
_connectionModeSection = new ConnectionModeSection(_root);
|
||||
_connectionModeSection.OnModeChanged += mode => OnConnectionModeChanged?.Invoke(mode);
|
||||
_connectionModeSection.OnFoldoutChanged += value => OnConfigurationFoldoutChanged?.Invoke(value);
|
||||
|
||||
_cliSetupSection = new CliSetupSection(_root);
|
||||
_cliSetupSection.SetupBindings();
|
||||
_cliSetupSection.OnRefreshCliVersion += () => OnRefreshCliVersion?.Invoke();
|
||||
_cliSetupSection.OnInstallCli += () => OnInstallCli?.Invoke();
|
||||
_cliSetupSection.OnInstallSkills += () => OnInstallSkills?.Invoke();
|
||||
_cliSetupSection.OnRefreshSkillsState += () => OnRefreshSkillsState?.Invoke();
|
||||
_cliSetupSection.OnSkillsTargetChanged += value => OnSkillsTargetChanged?.Invoke(value);
|
||||
_cliSetupSection.OnGroupSkillsChanged += value => OnGroupSkillsChanged?.Invoke(value);
|
||||
_cliSetupSection.OnUseProjectCliVersionChanged += value => OnUseProjectCliVersionChanged?.Invoke(value);
|
||||
|
||||
_mainScrollView = _root.Q<ScrollView>("main-scroll-view");
|
||||
ConfigureScrollView();
|
||||
|
||||
_cliContent = _root.Q<VisualElement>("cli-content");
|
||||
_mcpContent = _root.Q<VisualElement>("mcp-content");
|
||||
|
||||
_connectedToolsSection = new ConnectedToolsSection(_root);
|
||||
_connectedToolsSection.OnFoldoutChanged += value => OnConnectedToolsFoldoutChanged?.Invoke(value);
|
||||
|
||||
_editorConfigSection = new EditorConfigSection(_root);
|
||||
_editorConfigSection.OnEditorTypeChanged += value => OnEditorTypeChanged?.Invoke(value);
|
||||
_editorConfigSection.OnRepositoryRootChanged += value => OnRepositoryRootChanged?.Invoke(value);
|
||||
_editorConfigSection.OnConfigureClicked += () => OnConfigureClicked?.Invoke();
|
||||
_editorConfigSection.OnDeleteConfigClicked += () => OnDeleteConfigClicked?.Invoke();
|
||||
_editorConfigSection.OnOpenSettingsClicked += () => OnOpenSettingsClicked?.Invoke();
|
||||
|
||||
_toolSettingsSection = new ToolSettingsSection(_root);
|
||||
_toolSettingsSection.OnFoldoutChanged += value => OnToolSettingsFoldoutChanged?.Invoke(value);
|
||||
_toolSettingsSection.OnToolToggled += (toolName, enabled) => OnToolToggled?.Invoke(toolName, enabled);
|
||||
_toolSettingsSection.OnAllowThirdPartyChanged += value => OnAllowThirdPartyChanged?.Invoke(value);
|
||||
_toolSettingsSection.OnSecurityLevelChanged += value => OnSecurityLevelChanged?.Invoke(value);
|
||||
|
||||
_githubLinkRow = _root.Q<VisualElement>("github-link-row");
|
||||
_githubLinkLabel = _root.Q<Label>("github-link-label");
|
||||
_githubLinkIcon = _root.Q<Image>("github-link-icon");
|
||||
Debug.Assert(_githubLinkRow != null, "github-link-row must not be null");
|
||||
Debug.Assert(_githubLinkLabel != null, "github-link-label must not be null");
|
||||
Debug.Assert(_githubLinkIcon != null, "github-link-icon must not be null");
|
||||
_githubLinkRow.RegisterCallback<ClickEvent>(_ => HandleOpenGitHub());
|
||||
_githubLinkRow.RegisterCallback<MouseEnterEvent>(_ => HandleGitHubHoverChanged(true));
|
||||
_githubLinkRow.RegisterCallback<MouseLeaveEvent>(_ => HandleGitHubHoverChanged(false));
|
||||
InitializeGitHubIcon();
|
||||
}
|
||||
|
||||
private void ConfigureScrollView()
|
||||
{
|
||||
Debug.Assert(_mainScrollView != null, "mainScrollView must not be null");
|
||||
_mainScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto;
|
||||
_mainScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
|
||||
}
|
||||
|
||||
private void InitializeGitHubIcon()
|
||||
{
|
||||
string iconPath = $"{McpConstants.PackageAssetPath}/{GITHUB_ICON_RELATIVE_PATH}";
|
||||
Texture2D iconTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(iconPath);
|
||||
Debug.Assert(iconTexture != null, $"GitHub icon not found at {iconPath}");
|
||||
_githubLinkIcon.image = iconTexture;
|
||||
}
|
||||
|
||||
private static void HandleOpenGitHub()
|
||||
{
|
||||
Application.OpenURL(McpUIConstants.PROJECT_REPOSITORY_URL);
|
||||
}
|
||||
|
||||
private void HandleGitHubHoverChanged(bool isHovered)
|
||||
{
|
||||
ViewDataBinder.ToggleClass(_githubLinkRow, "mcp-github-link--hover", isHovered);
|
||||
ViewDataBinder.ToggleClass(_githubLinkLabel, "mcp-github-link__label--hover", isHovered);
|
||||
ViewDataBinder.ToggleClass(_githubLinkIcon, "mcp-github-link__icon--hover", isHovered);
|
||||
}
|
||||
|
||||
public void UpdateConnectedTools(ConnectedToolsData data)
|
||||
{
|
||||
_connectedToolsSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateEditorConfig(EditorConfigData data)
|
||||
{
|
||||
_editorConfigSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateToolSettings(ToolSettingsSectionData data)
|
||||
{
|
||||
_toolSettingsSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateSingleToolToggle(string toolName, bool enabled)
|
||||
{
|
||||
_toolSettingsSection?.UpdateSingleToggle(toolName, enabled);
|
||||
}
|
||||
|
||||
public void UpdateConnectionMode(ConnectionModeData data)
|
||||
{
|
||||
_connectionModeSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateConfigurationFoldout(bool show)
|
||||
{
|
||||
_connectionModeSection?.UpdateFoldout(show);
|
||||
}
|
||||
|
||||
public void UpdateCliSetup(CliSetupData data)
|
||||
{
|
||||
_cliSetupSection?.Update(data);
|
||||
}
|
||||
|
||||
public void UpdateSectionVisibility(ConnectionMode mode)
|
||||
{
|
||||
bool isMcp = mode == ConnectionMode.MCP;
|
||||
|
||||
ViewDataBinder.SetVisible(_mcpContent, isMcp);
|
||||
ViewDataBinder.SetVisible(_cliContent, !isMcp);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connectionModeSection = null;
|
||||
_cliSetupSection = null;
|
||||
_connectedToolsSection = null;
|
||||
_editorConfigSection = null;
|
||||
_toolSettingsSection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba059c58126bd44769bae90e953d8c3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user