feat(autotell): wire temp-tab pop-outs to the channel-popout pool

This commit is contained in:
2026-06-16 00:39:01 +02:00
parent 8e2d333130
commit 7b6871fea4
+33 -15
View File
@@ -218,7 +218,7 @@ internal sealed class AutoTellTabsService : IDisposable
return null;
}
private static Tab? FindTempTab(string name, uint world)
internal static Tab? FindTempTab(string name, uint world)
{
var byTarget = Plugin.Config.Tabs.FirstOrDefault(t =>
t.IsTempTab
@@ -256,21 +256,20 @@ internal sealed class AutoTellTabsService : IDisposable
return;
}
// Pop-out-window cleanup is offline while the channel-popout pool
// is rebuilt — Tab.PopOut still flips on/off, the visible window
// disappears once the new pool comes online.
var dropped = victim.Tab;
Plugin.Config.Tabs.RemoveAt(victim.Index);
// Re-anchor the UI selection if it pointed at the dropped tab. This runs on
// the PendingMessage worker thread and the repair mutates the re-seeded
// tab's channel via OnTabActivated, so marshal it onto the framework thread
// to serialize with Draw (reference_dalamud_framework_thread) — otherwise a
// half-applied strip could race the input bar's send-routing read.
// Re-anchor the UI selection if it pointed at the dropped tab, and close any
// pop-out window the dropped tab owned. Both run on the PendingMessage worker
// thread and touch window state the Draw path reads (OnTabActivated re-seed +
// the pool's Unbind), so marshal onto the framework thread to serialize with
// Draw (reference_dalamud_framework_thread). TryClose is idempotent: a tab that
// was never popped is a silent no-op.
Plugin.Framework.RunOnFrameworkThread(() =>
_plugin.MainWindow?.ResetActiveTabIfRemoved(dropped)
);
{
_plugin.ChannelPopoutPool.TryClose(dropped.Identifier);
_plugin.MainWindow?.ResetActiveTabIfRemoved(dropped);
});
}
private void SpawnTempTab((string Name, uint World) partner, Message currentMessage)
@@ -282,13 +281,28 @@ internal sealed class AutoTellTabsService : IDisposable
tab.AddMessage(currentMessage, unread: true);
// Open as pop-out if configured (set before Tabs.Add for next render-tick)
// Open as pop-out if configured (flag set before Tabs.Add for the next render-tick).
if (Plugin.Config.AutoTellTabsOpenAsPopout)
{
tab.PopOut = true;
}
Plugin.Config.Tabs.Add(tab);
// Actually open the pop-out window for the flagged tab — without this the
// flag was dead (a PopOut tab with no window). SpawnTempTab runs on the
// PendingMessage worker thread under _tempTabsLock; TryOpen does
// OnTabActivated + Bind (window state Draw reads), so marshal onto the
// framework thread. If the pool is full, drop the flag so it never claims a
// window it didn't get (flag/window parity).
if (tab.PopOut)
{
Plugin.Framework.RunOnFrameworkThread(() =>
{
if (!_plugin.ChannelPopoutPool.TryOpen(tab))
tab.PopOut = false;
});
}
}
private static Tab BuildTempTab(string playerName, uint worldRowId)
@@ -427,8 +441,12 @@ internal sealed class AutoTellTabsService : IDisposable
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
.Select(t => t.Identifier)
.ToList();
// Pop-out-window cleanup is offline; see Disconnect path above.
_ = poppedTempTabIds;
// Close each popped temp tab's window before the tabs leave the list.
// Logout is a framework-thread event (serialized with Draw), so no
// marshalling is needed here, unlike the worker-thread eviction path.
foreach (var id in poppedTempTabIds)
_plugin.ChannelPopoutPool.TryClose(id);
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);