fix(tell): strip stale tell state on tab activation

OnTabActivated clears the runtime tell state the game-side detour leaves on a tab (CurrentChannel tell target + partner label) when a DIFFERENT tab becomes the input surface, so a normal typed line can no longer route as a silent /tell to the old partner. Re-clicking the active tab and tabs carrying their own Tab.TellTarget binding (leg1) are preserved.

All four activation paths route through it: Sidebar, TopTabBar, ChannelPopoutPool.TryOpen, and the MainWindow draw-seed. EnsureCurrentChannel becomes a pure derive-helper reached only via OnTabActivated. Adds the TellResetOnActivateStep self-test (step count 29 -> 30).
This commit is contained in:
2026-06-13 15:09:38 +02:00
parent 593eb30c9f
commit ad635c77c1
7 changed files with 197 additions and 5 deletions
+2 -1
View File
@@ -247,8 +247,9 @@ internal sealed class Sidebar
var rowHovered = ImGui.IsItemHovered();
if (ImGui.IsItemClicked())
{
var previous = activeTab;
activeTab = tab;
TabLifecycleHelpers.EnsureCurrentChannel(tab);
TabLifecycleHelpers.OnTabActivated(tab, previous);
}
dl.DrawHoverSheen(
+2 -1
View File
@@ -34,8 +34,9 @@ internal sealed class TopTabBar
)
)
{
var previous = activeTab;
activeTab = tab;
TabLifecycleHelpers.EnsureCurrentChannel(tab);
TabLifecycleHelpers.OnTabActivated(tab, previous);
}
TabContextMenu.Draw(tab, $"toptab_ctx_{i}", _pool);
@@ -1,3 +1,4 @@
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Windows;
@@ -40,6 +41,12 @@ internal sealed class ChannelPopoutPool
public bool TryOpen(Tab tab)
{
// A popped tab gets its own input bar, so strip stale tell state first —
// otherwise a popped-out stale-tell tab would be a send surface that
// bypasses the click-path activation strip. Previous = the main window's
// active tab; popping the active tab itself must not strip (TR-4 guard).
TabLifecycleHelpers.OnTabActivated(tab, Plugin.Instance.MainWindow?.ActiveTab);
var slot = _slots.TryReserve(tab.Identifier);
if (slot < 0)
{
+7 -1
View File
@@ -156,7 +156,13 @@ internal sealed class MainWindow : Window
// First-frame seed: the active tab defaults to the first persisted
// tab so the message list isn't empty on a clean session.
if (_activeTab is null && Plugin.Config.Tabs.Count > 0)
_activeTab = Plugin.Config.Tabs[0];
{
var seeded = Plugin.Config.Tabs[0];
_activeTab = seeded;
// The seeded Tabs[0] is the likeliest legacy stale-tell carrier
// (pre-coupling the detour wrote here); strip it like any activation.
TabLifecycleHelpers.OnTabActivated(seeded, null);
}
var statusHeight = Components.StatusBar.Height;