From d5735d8dccd0590e0d68fecb0994b439394e4e11 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 13 May 2026 10:31:21 +0200 Subject: [PATCH] fix(tabs): preserve runtime channel across Settings Save, deep-clone seeded CurrentChannel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke-test round 4 surfaced a clean reproducer: a Party or Linkshell tab with channel /p, then Settings → Save, popped the input back to /tell on the next interaction. Two bugs combined: 1. Configuration.UpdateFrom captured only Messages+LastSendUnread from the live state during the persistent-tab merge. CurrentChannel was not preserved, so a Settings save overwrote the runtime channel state with the settings-time snapshot. If the user switched channel in-game between Settings-open and Settings-save, that switch was lost. Live CurrentChannel now joins Messages and LastSendUnread in the per-Identifier preservation tuple. 2. TabSwitched seeded a new tab's CurrentChannel from previousTab via reference copy (`newTab.CurrentChannel = previousTab.CurrentChannel`). That left both tabs sharing the same UsedChannel instance, so a later mutation on one bled into the other — exactly the path that carried a pinned tell-target onto Party. Switched to a deep clone (UsedChannel.Clone(), same Cherry-Pick-Patch-B pattern from v1.4.6) plus a Debug log so the next smoke can confirm at a glance which previous tab donated its channel state. Pre-existing ChatTwo upstream pattern; v1.4.7 just made it visible because pinned tabs are now the kind of long-lived tell-target that sticks around for the seed path to grab. --- HellionChat/Configuration.cs | 10 +++++++--- HellionChat/Ui/ChatLogWindow.cs | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 5381ed4..2cc1cda 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -293,11 +293,14 @@ public class Configuration : IPluginConfiguration // not destroy open tell conversations. Pinned TempTabs are persistent // and come through `other` like regular tabs; unpinned TempTabs are // session-only and held from the local state. For persistent tabs - // (incl. pinned), capture live MessageList + LastSendUnread by - // Identifier and restore them onto the freshly cloned tabs. + // (incl. pinned), capture live runtime state by Identifier and restore + // it onto the freshly cloned tabs — CurrentChannel is critical because + // the user may have switched channel in-game between settings-open + // and settings-save, and we'd otherwise overwrite that with the + // settings-time snapshot. var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList(); var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t)) - .ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread)); + .ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel)); Tabs = other .Tabs.Where(t => !t.IsTempTab || t.IsPinned) @@ -308,6 +311,7 @@ public class Configuration : IPluginConfiguration { clone.Messages = live.Messages; clone.LastSendUnread = live.LastSendUnread; + clone.CurrentChannel = live.CurrentChannel; } return clone; }) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index e67f85c..3d2a502 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -441,11 +441,24 @@ public sealed class ChatLogWindow : Window private void TabSwitched(Tab newTab, Tab previousTab) { - // Use the fixed channel if set by the user, or set it to the current tabs channel if this tab wasn't accessed before + // Use the fixed channel if set by the user. Otherwise, if the new tab + // has no channel state yet (fresh from JSON, never selected this + // session), seed from the previous tab — but deep-clone so we don't + // share TellTarget with the previous tab. Without the clone, a later + // /tell on the new tab would mutate the pinned tab's TellTarget and + // the Party/Linkshell channel would pop back to the pinned tell-mark. if (newTab.Channel is not null) + { newTab.CurrentChannel.Channel = newTab.Channel.Value; + } else if (newTab.CurrentChannel.Channel is InputChannel.Invalid) - newTab.CurrentChannel = previousTab.CurrentChannel; + { + newTab.CurrentChannel = previousTab.CurrentChannel.Clone(); + Plugin.LogProxy.Debug( + $"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' " + + $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})" + ); + } SetChannel(newTab.CurrentChannel.Channel); }