diff --git a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
index d652fe8c..f94d6c9c 100644
--- a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
+++ b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
@@ -16,7 +16,9 @@ namespace AcDream.App.UI.Layout;
/// m_pFellowship == null, and vice-versa. Both are AUTHORED sibling
/// containers of the fellowship page (0x10000292) — confirmed by the
/// FA3 live-mount probe: 0x1000026B holds the name-entry box, Create
-/// button, and all three visible option checkboxes; 0x10000275 holds
+/// button, and all FOUR visible option checkboxes (Ignore Fellowship
+/// Requests / Auto-Accept Requests / Share XP / Share Loot —
+/// 0x10000270–0x10000273, fix-round mechanism SF-4); 0x10000275 holds
/// the roster list and all six member-management buttons. A single
/// toggle on each container is therefore the
/// WHOLE empty-state swap; no per-child hiding is needed (closes lane-A
diff --git a/src/AcDream.App/UI/Layout/SocialPanelRowText.cs b/src/AcDream.App/UI/Layout/SocialPanelRowText.cs
index 85e824af..eb1595ac 100644
--- a/src/AcDream.App/UI/Layout/SocialPanelRowText.cs
+++ b/src/AcDream.App/UI/Layout/SocialPanelRowText.cs
@@ -16,9 +16,20 @@ namespace AcDream.App.UI.Layout;
///
internal static class SocialPanelRowText
{
- /// Returns the deepest descendant of
- /// (pre-order, last match wins), or null if the
- /// built row contains no text element at all.
+ ///
+ /// Returns the LAST found by a pre-order walk of
+ /// (row itself, then each child's own subtree in
+ /// order), or null if the built row contains no text element at all.
+ /// Fix-round mechanism SF-9: this equals the truly DEEPEST match
+ /// only when the subtree is a single chain — both real row templates
+ /// (Friends 0x10000519, Squelch 0x10000541) ARE single
+ /// chains (row root -> one Type-0xC text child -> one more Type-0xC
+ /// text grandchild, per the FA3 live-mount probe), so this method's
+ /// actual "last match in traversal order" behavior and the class doc's
+ /// "deepest" framing agree for both templates in production today. A
+ /// row template with a BRANCHING subtree (multiple text leaves at
+ /// different depths) would expose the difference.
+ ///
public static UiText? FindDeepest(UiElement row)
{
UiText? found = row as UiText;
diff --git a/src/AcDream.App/UI/UiTemplateListBox.cs b/src/AcDream.App/UI/UiTemplateListBox.cs
index 7da7f4de..1560f404 100644
--- a/src/AcDream.App/UI/UiTemplateListBox.cs
+++ b/src/AcDream.App/UI/UiTemplateListBox.cs
@@ -239,7 +239,10 @@ public sealed class UiTemplateListBox : UiDatElement
///
/// Campaign FA slice FA3: removes every row previously added via
/// /, resetting
- /// to 0 — the "Gap found" prerequisite lane A/D
+ /// to 0 AND the scroll position to 0
+ /// ( calls
+ /// Scroll.SetScrollY(0) — fix-round blast SF-5: the ORIGINAL doc here
+ /// only mentioned the height reset) — the "Gap found" prerequisite lane A/D
/// flagged (docs/research/2026-08-11-fa-panel-structure.md §6.6: "no Flush, no
/// selection model, no per-row instance-id"). Needed for a poll-and-rebuild
/// binding (Friends/Squelch read-only lists) where the row COUNT can shrink
@@ -247,6 +250,20 @@ public sealed class UiTemplateListBox : UiDatElement
/// grows the stack. A no-op, never allocating the viewport, when the box is
/// still dormant (no row has ever been added) — mirrors every other dormancy
/// guard on this class (see class doc).
+ ///
+ ///
+ /// Diverges from the sibling it shares a
+ /// name with (fix-round blast SF-5): that method does NOT touch scroll
+ /// position. For a poll-and-rebuild Friends/Squelch list, this means a user
+ /// scrolled partway into a long roster is yanked back to the top on every
+ /// server-side change (a friend's online status flips, etc.) — a real,
+ /// user-visible UX cost once is actually
+ /// wired (it now is — see ).
+ /// Not fixed here: doing so would require this class to preserve/re-clamp
+ /// the offset across a rebuild whose row COUNT can shrink, which is a
+ /// bigger behavior change than a fix-round doc correction should make
+ /// unasked; flagged for whoever revisits Friends/Squelch scrolling next.
+ ///
///
public void Flush() => _viewport?.ClearContent();
}
diff --git a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
index 9025e47f..d4fecac9 100644
--- a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
@@ -67,6 +67,11 @@ public sealed class SocialPanelLiveMountProbeTests
tabs.ActivateTabBehavior();
Assert.Empty(tabs.UnresolvedEntries);
+ // Fix-round mechanism SF-3: page exclusivity was activated but never
+ // asserted — the #372 class ("the panel mounts but the pages are
+ // wrong/blank") deserves a real assertion, not just a hope.
+ bool sawVisiblePage = false;
+ string? visiblePageName = null;
foreach ((uint pageId, string name) in new[]
{
(0x10000513u, "Friends"),
@@ -77,9 +82,20 @@ public sealed class SocialPanelLiveMountProbeTests
{
UiElement? page = UiElement.FindDescendant(tabs, pageId);
Console.WriteLine(
- $"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)}");
+ $"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)} "
+ + $"Visible={page?.Visible}");
Assert.NotNull(page);
+ if (page!.Visible)
+ {
+ Assert.False(
+ sawVisiblePage,
+ $"page exclusivity violated: both '{visiblePageName}' and '{name}' report Visible=true after ActivateTabBehavior()");
+ sawVisiblePage = true;
+ visiblePageName = name;
+ }
}
+ Assert.True(sawVisiblePage, "no page reports Visible=true after ActivateTabBehavior()");
+ Assert.Equal("Allegiance", visiblePageName);
// Fellowship empty/full frame pair.
foreach ((uint id, string name) in new[]
@@ -116,6 +132,10 @@ public sealed class SocialPanelLiveMountProbeTests
Assert.NotNull(allegiancePage);
int passupCount = CountDescendants(allegiancePage!, 0x10000492u);
Console.WriteLine($"[socialprobe] 0x10000492 occurrences under allegiance page = {passupCount}");
+ // Fix-round mechanism SF-3: this was printed but never asserted — a
+ // future importer change that collapses or drops one instance must
+ // fail this test, not just the log.
+ Assert.Equal(2, passupCount);
// Tab button captions — non-empty (the #375 resolver class: a missing
// string resolver renders blank captions even though the layout mounts).