From 1869f752d9361a5e4bf6554a7b97a47376c1381b Mon Sep 17 00:00:00 2001 From: dehuaichendragonplus Date: Mon, 7 Sep 2026 11:06:33 +0800 Subject: [PATCH 1/3] feat(mcp-server): make Recent Activity rows collapsible by default Every row in the MCP Server window's Recent Activity panel previously rendered its full structured result (or embedded screenshot) at all times. On a busy session with dozens of tool calls this made the panel noticeably slower to lay out/repaint, and buried the one entry a user actually cared about under a wall of unrelated detail. - Collapse each row to a one-line summary by default; clicking the header row or the summary line expands/collapses it in place. - The most recently added row still auto-expands so the latest result stays visible without an extra click, but any row a user has manually expanded or collapsed keeps that state as newer entries arrive (only the automatic "latest" expansion is ever superseded, and only if the user hasn't touched that row). - The summary line now stretches to the panel's actual width on every relayout (grows back on widen, shrinks on narrow) instead of a fixed character cap that either truncated it short of the available space or couldn't grow into a wider window. - Hovering a row (header or summary line) shows the same formatted, multi-line detail text the expanded view renders. This intercepts the TooltipEvent during the trickle-down phase before Unity's default per-Label ellipsis tooltip -- which shows raw, unformatted text and could disagree with what the header showed -- gets a chance to supply it. --- CHANGELOG.md | 3 + .../Server/FunplayMCPRecentActivityPanel.cs | 120 +++++++++++++++++- 2 files changed, 117 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a75476..0848290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Changed +- The MCP Server window's Recent Activity panel now renders each entry collapsed to a one-line summary by default, instead of always rendering the full structured result (or image preview) for every row. On a busy session the panel previously accumulated dozens of fully-expanded cards -- each with its own parsed JSON tree or embedded screenshot -- which made the window noticeably slower to lay out and repaint, and buried the one entry a user actually wanted to check under a wall of detail from unrelated calls. Clicking a row's header or its summary line expands it in place; the most recently added entry still expands automatically so the latest result is visible without an extra click, but any entry a user has manually expanded or collapsed keeps that state even as newer entries arrive afterward (only the automatic "latest entry" expansion is superseded). The summary line stretches to the panel's actual width on every relayout -- growing back on widen as well as shrinking on narrow -- instead of a fixed character count that left it either truncated well short of the available space or unable to grow into a wider window. Hovering a row (its header or its summary line) shows the same formatted, multi-line detail text the expanded view renders, by intercepting the tooltip during the trickle-down phase before Unity's own default per-Label ellipsis tooltip (which otherwise shows raw, unformatted text and could disagree with what the header showed) has a chance to supply it. + ## [0.6.5] - 2026-09-03 ### Added diff --git a/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs b/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs index 43638d2..ac05818 100644 --- a/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs +++ b/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs @@ -53,6 +53,16 @@ internal sealed class FunplayMCPRecentActivityPanel : IDisposable private readonly MCPServerService _server; private readonly List _previewTextures = new List(); private ScrollView _scrollView; + private RowExpandState _autoExpandedRow; + + // 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 bool ManuallyToggled; + public Action ApplyExpanded; + } public FunplayMCPRecentActivityPanel(MCPServerService server) { @@ -102,9 +112,12 @@ public void AddTo(VisualElement parent) _scrollView.style.paddingBottom = 4; parent.Add(_scrollView); + _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 = _server.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) @@ -114,7 +127,7 @@ public void OnEntryAdded(MCPLogEntry entry) if (_scrollView == null) return; - AddRow(entry); + AddRow(entry, isLatest: true); EditorApplication.delayCall += () => { if (_scrollView != null) @@ -127,9 +140,10 @@ public void Dispose() { ClearPreviewTextures(); _scrollView = null; + _autoExpandedRow = null; } - private void AddRow(MCPLogEntry entry) + private void AddRow(MCPLogEntry entry, bool isLatest = false) { var badgeText = GetBadgeText(entry.Status); var accentColor = GetAccentColor(entry.Status); @@ -152,6 +166,13 @@ private void AddRow(MCPLogEntry entry) 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,30 +219,117 @@ private void AddRow(MCPLogEntry entry) isStructuredResult = true; } + var detailsContainer = new VisualElement(); + var hasDetails = false; + if (!string.IsNullOrEmpty(displayResult)) { if (isStructuredResult) { - card.Add(CreateStructuredResult(displayResult)); + detailsContainer.Add(CreateStructuredResult(displayResult)); } else { var summaryLabel = CreateWrappedLabel(displayResult, new Color(0.6f, 0.6f, 0.6f)); summaryLabel.style.fontSize = 11; summaryLabel.style.marginTop = 3; - card.Add(summaryLabel); + detailsContainer.Add(summaryLabel); } + hasDetails = true; } if (!string.IsNullOrEmpty(entry.ImageDataUri) && TryCreateImagePreview(entry.ImageDataUri, out var preview)) { - card.Add(preview); + detailsContainer.Add(preview); + hasDetails = true; + } + + if (isLatest) + { + // 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 (_autoExpandedRow != null && !_autoExpandedRow.ManuallyToggled) + _autoExpandedRow.ApplyExpanded(false); + _autoExpandedRow = null; + } + + if (hasDetails) + { + var collapsedSummary = CreateCollapsedSummaryLabel(entry.ResultSummary); + 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 => + { + evt.tooltip = displayResult; + evt.rect = card.worldBound; + evt.StopImmediatePropagation(); + }, TrickleDown.TrickleDown); + + var rowState = new RowExpandState(); + var expanded = false; + rowState.ApplyExpanded = value => + { + expanded = value; + expandArrow.text = expanded ? "▾" : "▸"; + detailsContainer.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; + collapsedSummary.style.display = expanded ? DisplayStyle.None : DisplayStyle.Flex; + }; + + EventCallback toggle = _ => + { + 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 (isLatest) + _autoExpandedRow = rowState; + + rowState.ApplyExpanded(isLatest); + } + else + { + expandArrow.style.display = DisplayStyle.None; } _scrollView?.contentContainer.Add(card); } + private static Label CreateCollapsedSummaryLabel(string resultSummary) + { + var text = string.IsNullOrEmpty(resultSummary) ? "" : resultSummary.Replace('\n', ' ').Trim(); + + var label = new Label(text); + 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) { var container = new VisualElement(); From d261d8100e504c56b6802ed12bcf66df76f7615d Mon Sep 17 00:00:00 2001 From: Winlife <110321103+Winlifes@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:03:21 +0800 Subject: [PATCH 2/3] fix(ui): lazily render recent activity with readable summaries Create detail controls and screenshot textures on expansion; release them on collapse, clear, eviction, and disposal. Preserve manual expansion choices and invalidate queued callbacks after clear or rebuild. Use formatted display text for summaries, retain the bounded longer plain-text display, and add 12 interactive Editor UI regression cases. Verified in Unity 2022.3.62f3c1: 34 focused tests passed; full EditMode suite 410 passed, 0 failed, 10 skipped. --- CHANGELOG.md | 10 + .../Server/FunplayMCPRecentActivityPanel.cs | 157 +++++--- Editor/MCP/Server/MCPInteractionLog.cs | 7 +- Tests/Editor/RecentActivityPanelTests.cs | 367 ++++++++++++++++++ Tests/Editor/RecentActivityPanelTests.cs.meta | 11 + 5 files changed, 493 insertions(+), 59 deletions(-) create mode 100644 Tests/Editor/RecentActivityPanelTests.cs create mode 100644 Tests/Editor/RecentActivityPanelTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb039c..a0c34fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ ### Changed - The MCP Server window's Recent Activity panel now renders each entry collapsed to a one-line summary by default, instead of always rendering the full structured result (or image preview) for every row. On a busy session the panel previously accumulated dozens of fully-expanded cards -- each with its own parsed JSON tree or embedded screenshot -- which made the window noticeably slower to lay out and repaint, and buried the one entry a user actually wanted to check under a wall of detail from unrelated calls. Clicking a row's header or its summary line expands it in place; the most recently added entry still expands automatically so the latest result is visible without an extra click, but any entry a user has manually expanded or collapsed keeps that state even as newer entries arrive afterward (only the automatic "latest entry" expansion is superseded). The summary line stretches to the panel's actual width on every relayout -- growing back on widen as well as shrinking on narrow -- instead of a fixed character count that left it either truncated well short of the available space or unable to grow into a wider window. Hovering a row (its header or its summary line) shows the same formatted, multi-line detail text the expanded view renders, by intercepting the tooltip during the trickle-down phase before Unity's own default per-Label ellipsis tooltip (which otherwise shows raw, unformatted text and could disagree with what the header showed) has a chance to supply it. +- 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 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 ac05818..7f3b7cf 100644 --- a/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs +++ b/Editor/MCP/Server/FunplayMCPRecentActivityPanel.cs @@ -50,28 +50,37 @@ 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 Action _defer; + private readonly List _rows = new List(); private ScrollView _scrollView; private RowExpandState _autoExpandedRow; + private int _generation; // 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) : this(server.InteractionLog) { - _server = server; + } + + internal FunplayMCPRecentActivityPanel(MCPInteractionLog interactionLog, Action defer = null) + { + _interactionLog = interactionLog ?? throw new ArgumentNullException(nameof(interactionLog)); + _defer = defer ?? (callback => EditorApplication.delayCall += () => callback()); } public void AddTo(VisualElement parent) { - ClearPreviewTextures(); + ClearRows(); var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; @@ -88,10 +97,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; @@ -100,6 +109,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; @@ -115,32 +125,33 @@ public void AddTo(VisualElement parent) _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 = _server.InteractionLog.GetEntries(); + var entries = _interactionLog.GetEntries(); for (int i = entries.Count - 1; i >= 0; 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, isLatest: true); - EditorApplication.delayCall += () => + _defer(() => { - if (_scrollView != null) + if (_scrollView != null && generation == _generation) _scrollView.scrollOffset = new Vector2(0, float.MaxValue); - }; - }; + }); + }); } public void Dispose() { - ClearPreviewTextures(); + ClearRows(); _scrollView = null; - _autoExpandedRow = null; } private void AddRow(MCPLogEntry entry, bool isLatest = false) @@ -149,6 +160,7 @@ private void AddRow(MCPLogEntry entry, bool isLatest = false) 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; @@ -163,6 +175,7 @@ private void AddRow(MCPLogEntry entry, bool isLatest = false) card.style.marginBottom = 3; var topRow = new VisualElement(); + topRow.name = "recent-activity-header"; topRow.style.flexDirection = FlexDirection.Row; topRow.style.alignItems = Align.Center; @@ -219,31 +232,9 @@ private void AddRow(MCPLogEntry entry, bool isLatest = false) isStructuredResult = true; } - var detailsContainer = new VisualElement(); - var hasDetails = false; - - if (!string.IsNullOrEmpty(displayResult)) - { - if (isStructuredResult) - { - detailsContainer.Add(CreateStructuredResult(displayResult)); - } - else - { - var summaryLabel = CreateWrappedLabel(displayResult, new Color(0.6f, 0.6f, 0.6f)); - summaryLabel.style.fontSize = 11; - summaryLabel.style.marginTop = 3; - detailsContainer.Add(summaryLabel); - } - hasDetails = true; - } - - if (!string.IsNullOrEmpty(entry.ImageDataUri) && - TryCreateImagePreview(entry.ImageDataUri, out var preview)) - { - detailsContainer.Add(preview); - hasDetails = true; - } + 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) { @@ -257,7 +248,9 @@ private void AddRow(MCPLogEntry entry, bool isLatest = false) if (hasDetails) { - var collapsedSummary = CreateCollapsedSummaryLabel(entry.ResultSummary); + // 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); @@ -276,18 +269,54 @@ private void AddRow(MCPLogEntry entry, bool isLatest = false) evt.StopImmediatePropagation(); }, TrickleDown.TrickleDown); - var rowState = new RowExpandState(); var expanded = false; + Texture2D previewTexture = null; + rowState.ReleaseDetails = () => + { + 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 = _ => + EventCallback toggle = evt => { + if (evt.button != 0) + return; rowState.ManuallyToggled = true; rowState.ApplyExpanded(!expanded); }; @@ -306,14 +335,26 @@ private void AddRow(MCPLogEntry entry, bool isLatest = false) 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('\n', ' ').Trim(); + 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); @@ -610,25 +651,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, @@ -645,19 +685,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/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/Tests/Editor/RecentActivityPanelTests.cs b/Tests/Editor/RecentActivityPanelTests.cs new file mode 100644 index 0000000..6686c24 --- /dev/null +++ b/Tests/Editor/RecentActivityPanelTests.cs @@ -0,0 +1,367 @@ +// Copyright (C) Funplay. Licensed under MIT. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Funplay.Editor.MCP.Server; +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 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; + _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(); + } + + [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