diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f3bf5..535a710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ## [Unreleased] +### Added +- **Funplay > MCP Settings > Recent Activity** now includes a per-project **Expand all entries by default** preference. It is enabled by default for new and previously unconfigured projects, persists across Editor reloads (including an explicit off choice), and updates the open panel immediately without restarting MCP. Manually expanded or collapsed rows retain their state until the panel is reopened. + +### Changed +- The MCP Server window's Recent Activity panel supports collapsing entries to one-line summaries by clicking their headers, and reopening them from either the header or summary. With **Expand all entries by default** disabled, history is collapsed and only the latest result expands automatically; manually expanded or collapsed rows keep their state as new entries arrive. Summaries resize with the panel's available width in both directions, and hovering either the header or summary consistently shows the same formatted, multi-line detail text. + +- Collapsed summaries use the human-readable formatted result instead of raw JSON and retain text beyond the compact resource summary's 200-character limit, while keeping the existing 4,000-character display bound. Plain-text results use that same display budget. +- Detail controls and screenshot textures are created only when a row expands and released when it collapses, is cleared, or is evicted. The panel mirrors the activity log's bounded capacity, and queued callbacks cannot resurrect cleared rows or duplicate rows after a rebuild. + +### Pull requests and issues +- [PR #57](https://github.com/FunplayAI/funplay-unity-mcp/pull/57): collapsible Recent Activity, with a persistent expand-all preference, readable summaries, lazy detail loading, resource cleanup, and Editor UI regression tests. +- No GitHub issues were closed by these changes. + +### Contributors +- Thanks [@dehuaichendragonplus](https://github.com/dehuaichendragonplus) for PR #57. + ## [0.6.6] - 2026-09-07 ### Added diff --git a/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs b/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs index 43638d2..6982a7c 100644 --- a/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs +++ b/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using Funplay.Editor.Settings; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; @@ -50,18 +51,47 @@ internal sealed class FunplayMCPRecentActivityPanel : IDisposable private static readonly Color PendingValueColor = new Color(0.95f, 0.68f, 0.25f); private static readonly Color MutedValueColor = new Color(0.56f, 0.56f, 0.56f); - private readonly MCPServerService _server; - private readonly List _previewTextures = new List(); + private readonly MCPInteractionLog _interactionLog; + private readonly ISettingsController _settingsController; + private readonly Action _defer; + private readonly List _rows = new List(); private ScrollView _scrollView; + private RowExpandState _autoExpandedRow; + private int _generation; + private bool _expandAllByDefault; + + // Tracks one row's expand/collapse so the "latest entry auto-expands" behavior can + // later collapse it again once a newer entry arrives - but only if the user never + // touched it themselves in the meantime (manual choices always win). + private sealed class RowExpandState + { + public VisualElement Card; + public bool ManuallyToggled; + public Action ApplyExpanded; + public Action ReleaseDetails; + } - public FunplayMCPRecentActivityPanel(MCPServerService server) + public FunplayMCPRecentActivityPanel(MCPServerService server, ISettingsController settingsController) + : this(server.InteractionLog, settingsController: settingsController) { - _server = server; + } + + internal FunplayMCPRecentActivityPanel( + MCPInteractionLog interactionLog, + Action defer = null, + ISettingsController settingsController = null) + { + _interactionLog = interactionLog ?? throw new ArgumentNullException(nameof(interactionLog)); + _defer = defer ?? (callback => EditorApplication.delayCall += () => callback()); + _settingsController = settingsController; + _expandAllByDefault = _settingsController?.MCPRecentActivityExpandedByDefault ?? true; + if (_settingsController != null) + _settingsController.OnSettingsChanged += RefreshExpansionDefaults; } public void AddTo(VisualElement parent) { - ClearPreviewTextures(); + ClearRows(); var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; @@ -78,10 +108,10 @@ public void AddTo(VisualElement parent) var clearButton = new Button(() => { - _server.InteractionLog.Clear(); - ClearPreviewTextures(); - _scrollView?.contentContainer.Clear(); + _interactionLog.Clear(); + ClearRows(); }); + clearButton.name = "recent-activity-clear"; clearButton.text = "Clear"; clearButton.style.height = 20; clearButton.style.width = 50; @@ -90,6 +120,7 @@ public void AddTo(VisualElement parent) parent.Add(header); _scrollView = new ScrollView(ScrollViewMode.Vertical); + _scrollView.name = "recent-activity-scroll"; _scrollView.style.flexGrow = 1; _scrollView.style.backgroundColor = new Color(0.14f, 0.14f, 0.14f); _scrollView.style.borderTopLeftRadius = 4; @@ -102,39 +133,62 @@ public void AddTo(VisualElement parent) _scrollView.style.paddingBottom = 4; parent.Add(_scrollView); - var entries = _server.InteractionLog.GetEntries(); + _autoExpandedRow = null; + // GetEntries() returns newest-first (index 0 = most recent); this loop walks it + // back-to-front so the oldest row is added (and rendered) first, newest last/bottom. + var entries = _interactionLog.GetEntries(); for (int i = entries.Count - 1; i >= 0; i--) - AddRow(entries[i]); + AddRow(entries[i], isLatest: i == 0); } public void OnEntryAdded(MCPLogEntry entry) { - EditorApplication.delayCall += () => + var generation = _generation; + _defer(() => { - if (_scrollView == null) + // Clear/rebuild/dispose invalidates already-queued entries and scroll requests. + if (_scrollView == null || generation != _generation) return; - AddRow(entry); - EditorApplication.delayCall += () => + AddRow(entry, isLatest: true); + _defer(() => { - if (_scrollView != null) + if (_scrollView != null && generation == _generation) _scrollView.scrollOffset = new Vector2(0, float.MaxValue); - }; - }; + }); + }); } public void Dispose() { - ClearPreviewTextures(); + if (_settingsController != null) + _settingsController.OnSettingsChanged -= RefreshExpansionDefaults; + ClearRows(); _scrollView = null; } - private void AddRow(MCPLogEntry entry) + private void RefreshExpansionDefaults() + { + var expandAll = _settingsController.MCPRecentActivityExpandedByDefault; + if (expandAll == _expandAllByDefault) + return; + + _expandAllByDefault = expandAll; + // Apply the preference live without rebuilding rows or discarding manual choices. + foreach (var row in _rows) + { + if (!row.ManuallyToggled) + row.ApplyExpanded?.Invoke(expandAll || row == _autoExpandedRow); + } + } + + private void AddRow(MCPLogEntry entry, bool isLatest = false) { var badgeText = GetBadgeText(entry.Status); var accentColor = GetAccentColor(entry.Status); var card = new VisualElement(); + card.name = "recent-activity-row"; card.style.backgroundColor = new Color(0.19f, 0.19f, 0.19f); card.style.borderTopLeftRadius = 4; card.style.borderTopRightRadius = 4; @@ -149,9 +203,17 @@ private void AddRow(MCPLogEntry entry) card.style.marginBottom = 3; var topRow = new VisualElement(); + topRow.name = "recent-activity-header"; topRow.style.flexDirection = FlexDirection.Row; topRow.style.alignItems = Align.Center; + var expandArrow = new Label("▸"); + expandArrow.style.fontSize = 10; + expandArrow.style.color = new Color(0.5f, 0.5f, 0.5f); + expandArrow.style.marginRight = 4; + expandArrow.style.width = 10; + topRow.Add(expandArrow); + var timeLabel = new Label(entry.Timestamp.ToString("HH:mm:ss")); timeLabel.style.fontSize = 10; timeLabel.style.color = new Color(0.5f, 0.5f, 0.5f); @@ -198,28 +260,143 @@ private void AddRow(MCPLogEntry entry) isStructuredResult = true; } - if (!string.IsNullOrEmpty(displayResult)) + var detailsContainer = new VisualElement { name = "recent-activity-details" }; + var hasDetails = !string.IsNullOrEmpty(displayResult) || !string.IsNullOrEmpty(entry.ImageDataUri); + var rowState = new RowExpandState { Card = card }; + + if (isLatest) { - if (isStructuredResult) + // A new entry became the latest: collapse whichever row was auto-expanded for + // that reason before, unless the user has since touched it themselves - + // manual choices are never overridden by the "latest" auto-expand behavior. + if (!_expandAllByDefault && _autoExpandedRow != null && !_autoExpandedRow.ManuallyToggled) + _autoExpandedRow.ApplyExpanded(false); + _autoExpandedRow = null; + } + + if (hasDetails) + { + // DisplayResult is readable text with a larger, bounded display budget. The + // compact ResultSummary is raw protocol text capped at 200 chars for resources. + var collapsedSummary = CreateCollapsedSummaryLabel(displayResult); + card.Add(collapsedSummary); + + card.Add(detailsContainer); + + // Native Editor tooltip: show the same formatted detail text the expanded view + // renders, everywhere in the card. Plain `card.tooltip` is not enough: an ellipsis- + // truncated Label (the collapsed summary line) supplies its own default tooltip + // (its raw, unformatted text) at the moment it is hovered, which pre-empts an + // ancestor's tooltip and made the header and the summary line disagree. Intercept + // the TooltipEvent during the trickle-down (capture) phase instead, before it + // reaches any such child, so this one formatted value always wins. + card.RegisterCallback(evt => { - card.Add(CreateStructuredResult(displayResult)); - } - else + evt.tooltip = displayResult; + evt.rect = card.worldBound; + evt.StopImmediatePropagation(); + }, TrickleDown.TrickleDown); + + var expanded = false; + Texture2D previewTexture = null; + rowState.ReleaseDetails = () => { - var summaryLabel = CreateWrappedLabel(displayResult, new Color(0.6f, 0.6f, 0.6f)); - summaryLabel.style.fontSize = 11; - summaryLabel.style.marginTop = 3; - card.Add(summaryLabel); - } - } + detailsContainer.Clear(); + if (previewTexture != null) + UnityEngine.Object.DestroyImmediate(previewTexture); + previewTexture = null; + }; + rowState.ApplyExpanded = value => + { + if (value && !expanded) + { + if (!string.IsNullOrEmpty(displayResult)) + { + if (isStructuredResult) + detailsContainer.Add(CreateStructuredResult(displayResult)); + else + { + var text = CreateWrappedLabel(displayResult, new Color(0.6f, 0.6f, 0.6f)); + text.style.fontSize = 11; + text.style.marginTop = 3; + detailsContainer.Add(text); + } + } + + if (TryCreateImagePreview(entry.ImageDataUri, out var preview)) + { + previewTexture = preview.image as Texture2D; + detailsContainer.Add(preview); + } + } + else if (!value && expanded) + { + // Hiding the subtree alone retains its Labels and decoded GPU texture. + // Recreate on demand so collapsed history stays lightweight. + rowState.ReleaseDetails(); + } + expanded = value; + expandArrow.text = expanded ? "▾" : "▸"; + detailsContainer.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; + collapsedSummary.style.display = expanded ? DisplayStyle.None : DisplayStyle.Flex; + }; + + EventCallback toggle = evt => + { + if (evt.button != 0) + return; + rowState.ManuallyToggled = true; + rowState.ApplyExpanded(!expanded); + }; + // Both the header row and the collapsed summary line must open the card - + // they are siblings, not nested, so each needs its own click registration. + topRow.RegisterCallback(toggle); + collapsedSummary.RegisterCallback(toggle); - if (!string.IsNullOrEmpty(entry.ImageDataUri) && - TryCreateImagePreview(entry.ImageDataUri, out var preview)) + if (isLatest) + _autoExpandedRow = rowState; + + rowState.ApplyExpanded(_expandAllByDefault || isLatest); + } + else { - card.Add(preview); + expandArrow.style.display = DisplayStyle.None; } + _rows.Add(rowState); _scrollView?.contentContainer.Add(card); + // The log is a ring buffer; mirror its bound instead of retaining evicted UI rows. + while (_rows.Count > _interactionLog.Capacity) + { + var oldest = _rows[0]; + oldest.ReleaseDetails?.Invoke(); + oldest.Card.RemoveFromHierarchy(); + if (_autoExpandedRow == oldest) + _autoExpandedRow = null; + _rows.RemoveAt(0); + } + } + + private static Label CreateCollapsedSummaryLabel(string resultSummary) + { + var text = string.IsNullOrEmpty(resultSummary) ? "" : resultSummary.Replace('\r', ' ').Replace('\n', ' ').Trim(); + + var label = new Label(text); + label.name = "recent-activity-summary"; + label.enableRichText = false; + label.style.fontSize = 10; + label.style.color = new Color(0.55f, 0.55f, 0.55f); + label.style.marginTop = 2; + label.style.whiteSpace = WhiteSpace.NoWrap; + label.style.overflow = Overflow.Hidden; + label.style.textOverflow = TextOverflow.Ellipsis; + // No manual char cap and no explicit width: the label relies on the default + // stretch-to-parent cross-axis sizing (same as topRow) so it tracks the card's + // actual width on every relayout, growing back on widen as well as shrinking on + // narrow. minWidth=0 stops the NoWrap text's intrinsic width from acting as a + // lower bound that would otherwise fight that resize in one direction. + label.style.minWidth = 0; + return label; } private static VisualElement CreateStructuredResult(string displayResult) @@ -502,25 +679,24 @@ private static Color GetAccentColor(MCPToolCallStatus status) } } - private bool TryCreateImagePreview(string imageDataUri, out Image preview) + private static bool TryCreateImagePreview(string imageDataUri, out Image preview) { preview = null; const string prefix = "data:image/png;base64,"; if (string.IsNullOrEmpty(imageDataUri) || !imageDataUri.StartsWith(prefix, StringComparison.Ordinal)) return false; + Texture2D texture = null; try { var bytes = Convert.FromBase64String(imageDataUri.Substring(prefix.Length)); - var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); + texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); if (!texture.LoadImage(bytes)) { UnityEngine.Object.DestroyImmediate(texture); return false; } - _previewTextures.Add(texture); - preview = new Image { image = texture, @@ -537,19 +713,20 @@ private bool TryCreateImagePreview(string imageDataUri, out Image preview) } catch { + if (texture != null) + UnityEngine.Object.DestroyImmediate(texture); return false; } } - private void ClearPreviewTextures() + private void ClearRows() { - foreach (var texture in _previewTextures) - { - if (texture != null) - UnityEngine.Object.DestroyImmediate(texture); - } - - _previewTextures.Clear(); + _generation++; + foreach (var row in _rows) + row.ReleaseDetails?.Invoke(); + _rows.Clear(); + _autoExpandedRow = null; + _scrollView?.contentContainer.Clear(); } } } diff --git a/Editor/MCP/Server/FunplayMCPWindow.cs b/Editor/MCP/Server/FunplayMCPWindow.cs index ec18319..2a050aa 100644 --- a/Editor/MCP/Server/FunplayMCPWindow.cs +++ b/Editor/MCP/Server/FunplayMCPWindow.cs @@ -99,7 +99,7 @@ private void BuildUI() BuildUI) .AddTo(mainContainer); - _activityPanel = new FunplayMCPRecentActivityPanel(_mcpServer); + _activityPanel = new FunplayMCPRecentActivityPanel(_mcpServer, _settingsController); _activityPanel.AddTo(mainContainer); } diff --git a/Editor/MCP/Server/FunplayPluginSettingsWindow.cs b/Editor/MCP/Server/FunplayPluginSettingsWindow.cs index 7f57a8f..a38b90c 100644 --- a/Editor/MCP/Server/FunplayPluginSettingsWindow.cs +++ b/Editor/MCP/Server/FunplayPluginSettingsWindow.cs @@ -19,6 +19,7 @@ namespace Funplay.Editor.MCP.Server internal class FunplayPluginSettingsWindow : EditorWindow { private ISettingsController _settingsController; + private Toggle _recentActivityExpandedToggle; private Toggle _debugLoggingToggle; private Label _debugStatusLabel; @@ -32,6 +33,8 @@ public static void ShowWindow() public void CreateGUI() { + if (_settingsController != null) + _settingsController.OnSettingsChanged -= RefreshStatus; _settingsController = RootScopeServices.Services?.GetService(typeof(ISettingsController)) as ISettingsController; @@ -68,17 +71,41 @@ private void BuildUI() title.style.marginBottom = 4; rootVisualElement.Add(title); - var hint = new Label("Project-level settings for the Funplay Unity MCP plugin. Safety checks and debug logging are stored per project."); + var hint = new Label("Project-level settings for the Funplay Unity MCP plugin. Preferences are saved per project."); hint.style.fontSize = 11; hint.style.color = new Color(0.65f, 0.65f, 0.65f); hint.style.whiteSpace = WhiteSpace.Normal; hint.style.marginBottom = 10; rootVisualElement.Add(hint); + var content = new ScrollView(ScrollViewMode.Vertical) { name = "mcp-settings-scroll" }; + content.style.flexGrow = 1; + content.style.minHeight = 0; + rootVisualElement.Add(content); + var safetySection = CreateSection(); safetySection.style.marginBottom = 8; FunplayMCPSafetyPanel.AddTo(safetySection, _settingsController); - rootVisualElement.Add(safetySection); + content.Add(safetySection); + + var activitySection = CreateSection(); + activitySection.style.marginBottom = 8; + activitySection.Add(CreateSectionHeader("Recent Activity")); + _recentActivityExpandedToggle = new Toggle("Expand all entries by default") + { + name = "recent-activity-expanded-by-default" + }; + _recentActivityExpandedToggle.style.marginBottom = 5; + _recentActivityExpandedToggle.labelElement.style.whiteSpace = WhiteSpace.Normal; + _recentActivityExpandedToggle.labelElement.style.flexShrink = 1; + _recentActivityExpandedToggle.SetValueWithoutNotify(_settingsController.MCPRecentActivityExpandedByDefault); + _recentActivityExpandedToggle.RegisterValueChangedCallback(evt => + _settingsController.MCPRecentActivityExpandedByDefault = evt.newValue); + activitySection.Add(_recentActivityExpandedToggle); + activitySection.Add(CreateHint( + "On: expand all entries. Off: collapse history and expand only the latest entry. " + + "Applies immediately; manually expanded or collapsed entries keep your choice until the panel is reopened.")); + content.Add(activitySection); var debugSection = CreateSection(); debugSection.Add(CreateSectionHeader("Debug")); @@ -96,7 +123,7 @@ private void BuildUI() _debugStatusLabel = CreateHint(string.Empty); debugSection.Add(_debugStatusLabel); - rootVisualElement.Add(debugSection); + content.Add(debugSection); RefreshStatus(); } @@ -105,6 +132,8 @@ private void RefreshStatus() if (_settingsController == null) return; + _recentActivityExpandedToggle?.SetValueWithoutNotify(_settingsController.MCPRecentActivityExpandedByDefault); + var enabled = _settingsController.PluginDebugLoggingEnabled; if (_debugLoggingToggle != null) _debugLoggingToggle.SetValueWithoutNotify(enabled); @@ -123,6 +152,7 @@ private void RefreshStatus() private static VisualElement CreateSection() { var section = new VisualElement(); + section.style.flexShrink = 0; section.style.backgroundColor = new Color(0.14f, 0.14f, 0.14f); section.style.borderTopLeftRadius = 4; section.style.borderTopRightRadius = 4; diff --git a/Editor/MCP/Server/MCPInteractionLog.cs b/Editor/MCP/Server/MCPInteractionLog.cs index cf87bd6..8271e00 100644 --- a/Editor/MCP/Server/MCPInteractionLog.cs +++ b/Editor/MCP/Server/MCPInteractionLog.cs @@ -43,6 +43,7 @@ internal class MCPInteractionLog private readonly object _lock = new object(); public event Action OnEntryAdded; + internal int Capacity => _buffer.Length; public MCPInteractionLog(int capacity = 200) { @@ -57,7 +58,11 @@ public void Add(string toolName, MCPToolCallStatus status, string resultSummary) var compactSummary = imageDataUri != null ? "Screenshot captured successfully." : CreateCompactSummary(resultSummary); - var displayResult = compactSummary; + // Keep the compact resource/log summary bounded at 200 characters, but do not + // irreversibly truncate the panel's plain-text display at that smaller limit. + var displayResult = imageDataUri != null + ? compactSummary + : TruncateDisplayResult(resultSummary ?? ""); var isJsonResult = false; if (imageDataUri == null && TryFormatJsonForDisplay(resultSummary, out var formattedJson)) diff --git a/Editor/Settings/ISettingsController.cs b/Editor/Settings/ISettingsController.cs index 440738f..997c8dd 100644 --- a/Editor/Settings/ISettingsController.cs +++ b/Editor/Settings/ISettingsController.cs @@ -30,6 +30,7 @@ internal interface ISettingsController bool ExecuteCodeStrictFilesystemSafetyEnabled { get; set; } bool ExecuteCodeProjectNamespaceInjectionEnabled { get; set; } bool PluginDebugLoggingEnabled { get; set; } + bool MCPRecentActivityExpandedByDefault { get; set; } bool MCPBrokerModeEnabled { get; set; } string MCPBrokerMonoPath { get; set; } diff --git a/Editor/Settings/SettingsController.cs b/Editor/Settings/SettingsController.cs index 534a09f..10aa5c4 100644 --- a/Editor/Settings/SettingsController.cs +++ b/Editor/Settings/SettingsController.cs @@ -23,6 +23,7 @@ internal class SettingsController : ISettingsController private const bool DefaultExecuteCodeStrictFilesystemSafetyEnabled = true; private const bool DefaultExecuteCodeProjectNamespaceInjectionEnabled = false; private const bool DefaultPluginDebugLoggingEnabled = false; + private const bool DefaultMCPRecentActivityExpandedByDefault = true; private const bool DefaultMCPBrokerModeEnabled = false; private readonly string _settingsPath; @@ -254,6 +255,23 @@ public bool PluginDebugLoggingEnabled } } + public bool MCPRecentActivityExpandedByDefault + { + get + { + lock (_lock) + return _settings.mcpRecentActivityExpandedByDefault; + } + set + { + UpdateSettings(data => + { + data.mcpRecentActivityExpandedByDefault = value; + data.mcpRecentActivityExpansionConfigured = true; + }); + } + } + public bool MCPBrokerModeEnabled { get @@ -413,6 +431,8 @@ private static SettingsData CreateDefaultSettings() executeCodeProjectNamespaceInjectionConfigured = true, pluginDebugLoggingEnabled = DefaultPluginDebugLoggingEnabled, pluginDebugLoggingConfigured = true, + mcpRecentActivityExpandedByDefault = DefaultMCPRecentActivityExpandedByDefault, + mcpRecentActivityExpansionConfigured = true, mcpBrokerModeEnabled = DefaultMCPBrokerModeEnabled, mcpBrokerMonoPath = string.Empty }; @@ -468,6 +488,11 @@ private static void NormalizeInPlace(SettingsData settings) settings.pluginDebugLoggingEnabled = DefaultPluginDebugLoggingEnabled; settings.pluginDebugLoggingConfigured = true; } + if (!settings.mcpRecentActivityExpansionConfigured) + { + settings.mcpRecentActivityExpandedByDefault = DefaultMCPRecentActivityExpandedByDefault; + settings.mcpRecentActivityExpansionConfigured = true; + } } private static string NormalizeToolExportProfile(string value) @@ -519,6 +544,8 @@ private class SettingsData public bool executeCodeProjectNamespaceInjectionConfigured = false; public bool pluginDebugLoggingEnabled = DefaultPluginDebugLoggingEnabled; public bool pluginDebugLoggingConfigured = false; + public bool mcpRecentActivityExpandedByDefault = DefaultMCPRecentActivityExpandedByDefault; + public bool mcpRecentActivityExpansionConfigured = false; public bool mcpBrokerModeEnabled = DefaultMCPBrokerModeEnabled; public string mcpBrokerMonoPath = string.Empty; public List mcpLastClientConfigKeys; diff --git a/Tests/Editor/RecentActivityPanelTests.cs b/Tests/Editor/RecentActivityPanelTests.cs new file mode 100644 index 0000000..30bffc2 --- /dev/null +++ b/Tests/Editor/RecentActivityPanelTests.cs @@ -0,0 +1,499 @@ +// Copyright (C) Funplay. Licensed under MIT. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Funplay.Editor.MCP.Server; +using Funplay.Editor.Services; +using Funplay.Editor.Settings; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.TestTools; +using UnityEngine.UIElements; + +namespace Funplay.Editor.Tests +{ + public sealed class RecentActivityPanelTests + { + private EditorWindow _window; + private MCPInteractionLog _log; + private FunplayMCPRecentActivityPanel _panel; + private string _settingsProjectPath; + private readonly Queue _callbacks = new Queue(); + + [UnitySetUp] + public IEnumerator SetUp() + { + if (Application.isBatchMode || + SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null) + Assert.Ignore("These UI tests require a graphics-enabled interactive Unity Editor."); + _log = null; + _panel = null; + _settingsProjectPath = null; + _callbacks.Clear(); + _window = ScriptableObject.CreateInstance(); + _window.titleContent = new GUIContent("Funplay Activity Tests"); + _window.ShowUtility(); + _window.position = new Rect(80, 80, 480, 640); + yield return null; + } + + [TearDown] + public void TearDown() + { + if (_log != null && _panel != null) + _log.OnEntryAdded -= _panel.OnEntryAdded; + _panel?.Dispose(); + _callbacks.Clear(); + if (_window != null) + _window.Close(); + if (_settingsProjectPath != null && Directory.Exists(_settingsProjectPath)) + Directory.Delete(_settingsProjectPath, true); + } + + [Test] + public void History_OnlyLatestBuildsDetails_AndSummariesAreReadable() + { + _log = new MCPInteractionLog(); + _log.Add("first", MCPToolCallStatus.Success, + "{\"success\":true,\"message\":\"Imported assets.\",\"data\":{\"count\":3}}"); + _log.Add("second", MCPToolCallStatus.Success, "Second result"); + BuildPanel(); + + var rows = Rows(); + Assert.AreEqual(2, rows.Length); + AssertCollapsed(rows[0]); + AssertExpanded(rows[1]); + StringAssert.Contains("Imported assets.", Summary(rows[0]).text); + StringAssert.Contains("Count: 3", Summary(rows[0]).text); + StringAssert.DoesNotContain("\"success\"", Summary(rows[0]).text); + StringAssert.DoesNotContain("{", Summary(rows[0]).text); + + Click(Summary(rows[0])); + AssertExpanded(rows[0]); + Assert.IsTrue(Details(rows[0]).Query