chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user