From 8e38e3e805700bf127be14ed2a8613c12169035c Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 18 Aug 2026 23:36:54 +0200 Subject: [PATCH] refactor(config): delete what was never read, restore what was only orphaned Block D of v1.12.0. The line between the two is the whole job, and I got it wrong once on the way: six cache fields on Tab looked dead because nothing writes them, and nothing writes them because AutoTellTabTint and TabTintCache went out with the chat window in cf4705e. Deleting the fields would have cemented a loss instead of recording a decision. So they are back, and the sidebar uses them again: an auto-tell tab is tinted and glyphed from its partner, twelve colours against seven icons. Four open tells are no longer four identical envelopes in one colour. Their own header promised the same partner keeps its colour "across sessions" while hashing with string.GetHashCode, which .NET salts per process -- every game start reshuffled every tab. FNV-1a now, with a lowbias32 finalizer that is not decoration: without it a probe over 144 similar keys reached six of the twelve colours, because the caller takes the low bits with a modulo and FNV leaves those correlated. Three pinned values guard it, which is also the only assertion that can catch a regression to a salted hash. The same question, asked of the three hide conditions this block had quietly orphaned: HideDuringCutscenes, HideInBattle, HideWhenNotLoggedIn all had readers in v1.5.6 and lost them in the same commit. Two of them are states rather than conditions -- a cutscene the user dismissed stays dismissed until it ends, and combat must not seize a chat that is already hidden for another reason -- so they come back as a small state machine with eight pinned transitions, and three toggles whose labels were already translated in all 25 languages. Actually deleted, with a reader search each time: - Six per-tab hide fields. Their reader was the pop-out window and it stopped consulting them in cf4705e. Per-tab was the wrong unit anyway: "hide during cutscenes" is a statement about the screen. - Tab.ChatCodes, whose migration the v16 schema gate had already made unreachable. - InactivityHideTimeout and InactivityHideActiveDuringBattle, MaxLinesToRender which had stopped bounding anything, and the 155 lines of Configuration.UpdateFrom with no caller at all. Config version 25, at all three places that carry it. No migration step: the gate only refuses anything under 16 and Json.NET drops keys it does not know, so the deleted fields simply stop being written. One thing a review pass caught that matters more than any of the above: the clone parity guard had gone hollow. It compares collections by count, ChatCodes was the only collection the probe seeded, and removing it left the guard comparing zero against zero. Verified by making Tab.Clone discard both remaining collections and watching every assertion stay green. The probe seeds them now, and the same sabotage fails as it should. --- HellionChat/Configuration.cs | 197 ++---------------- HellionChat/Plugin.cs | 44 +++- .../Resources/HellionStrings.Designer.cs | 1 + HellionChat/Resources/HellionStrings.ca.resx | 3 + HellionChat/Resources/HellionStrings.cs.resx | 3 + HellionChat/Resources/HellionStrings.da.resx | 3 + HellionChat/Resources/HellionStrings.de.resx | 3 + HellionChat/Resources/HellionStrings.el.resx | 3 + HellionChat/Resources/HellionStrings.es.resx | 3 + HellionChat/Resources/HellionStrings.fi.resx | 3 + HellionChat/Resources/HellionStrings.fr.resx | 3 + HellionChat/Resources/HellionStrings.hu.resx | 3 + HellionChat/Resources/HellionStrings.it.resx | 3 + HellionChat/Resources/HellionStrings.ja.resx | 3 + HellionChat/Resources/HellionStrings.ko.resx | 3 + HellionChat/Resources/HellionStrings.nb.resx | 3 + HellionChat/Resources/HellionStrings.nl.resx | 3 + HellionChat/Resources/HellionStrings.pl.resx | 3 + .../Resources/HellionStrings.pt-BR.resx | 3 + .../Resources/HellionStrings.pt-PT.resx | 3 + HellionChat/Resources/HellionStrings.resx | 3 + HellionChat/Resources/HellionStrings.ro.resx | 3 + HellionChat/Resources/HellionStrings.ru.resx | 3 + HellionChat/Resources/HellionStrings.sv.resx | 3 + HellionChat/Resources/HellionStrings.tr.resx | 3 + HellionChat/Resources/HellionStrings.uk.resx | 3 + .../Resources/HellionStrings.zh-Hans.resx | 3 + .../Resources/HellionStrings.zh-Hant.resx | 3 + ...onV24Step.cs => ConfigMigrationV25Step.cs} | 10 +- HellionChat/Ui/AutoTellTabTint.cs | 97 +++++++++ HellionChat/Ui/Components/MessageList.cs | 7 +- .../Ui/Components/Settings/Tabs/WindowTab.cs | 33 ++- HellionChat/Ui/Components/Sidebar.cs | 29 ++- HellionChat/Ui/FirstRunWizard.cs | 14 +- HellionChat/Ui/TabTintCache.cs | 38 ++++ HellionChat/Ui/Windows/MainWindow.cs | 4 + HellionChat/Util/ChatHideState.cs | 76 +++++++ HellionChat/Util/LayoutFingerprint.cs | 4 +- HellionChat/Util/TabLifecycleHelpers.cs | 2 +- 39 files changed, 424 insertions(+), 207 deletions(-) rename HellionChat/SelfTests/{ConfigMigrationV24Step.cs => ConfigMigrationV25Step.cs} (92%) create mode 100644 HellionChat/Ui/AutoTellTabTint.cs create mode 100644 HellionChat/Ui/TabTintCache.cs create mode 100644 HellionChat/Util/ChatHideState.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index c1291c5..94427ac 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -35,7 +35,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - internal const int LatestVersion = 24; + internal const int LatestVersion = 25; public int Version { get; set; } = LatestVersion; @@ -184,8 +184,6 @@ public class Configuration : IPluginConfiguration // v1.2.1: default flipped false → true for consistency with other hide defaults. public bool HideInNewGamePlusMenu = true; public bool HideWhenInactive; - public int InactivityHideTimeout = 10; - public bool InactivityHideActiveDuringBattle = true; [Obsolete("Use InactivityHideChannelsV2 instead")] public Dictionary InactivityHideChannels = []; @@ -243,7 +241,6 @@ public class Configuration : IPluginConfiguration // UI-11: warn before sending a message that carries plugin-only glyphs. public bool NotifyPluginDisclosure = true; public bool KeepInputFocus = true; - public int MaxLinesToRender = 2_500; // 1-10000 public bool Use24HourClock = true; public bool ShowEmotes = true; public HashSet BlockedEmotes = []; @@ -310,164 +307,6 @@ public class Configuration : IPluginConfiguration // v22 field: MainWindow layout mode (sidebar vs. horizontal top tabs). // Initializer doubles as the migration default for configs loaded at v21. public MainWindowLayoutMode MainWindowLayoutMode = MainWindowLayoutMode.Sidebar; - - public void UpdateFrom(Configuration other, bool backToOriginal) - { - if (backToOriginal) - { - // NOTE (v1.8.0): this only flips the PopOut flag back. If a future - // caller ever wires UpdateFrom(backToOriginal: true) to a live - // settings-cancel path, that CALL-SITE must also iterate - // ChannelPopoutPool.TryClose over the affected Tab.Identifiers, - // otherwise pool windows stay IsOpen=true while the flag is false - // (orphan window). The pool is not reachable from this POCO by design. - foreach (var tab in Tabs.Where(t => t.PopOut)) - tab.PopOut = false; - } - - HideChat = other.HideChat; - HideDuringCutscenes = other.HideDuringCutscenes; - HideWhenNotLoggedIn = other.HideWhenNotLoggedIn; - HideWhenUiHidden = other.HideWhenUiHidden; - HideInLoadingScreens = other.HideInLoadingScreens; - HideInBattle = other.HideInBattle; - HideInNewGamePlusMenu = other.HideInNewGamePlusMenu; - HideWhenInactive = other.HideWhenInactive; - InactivityHideTimeout = other.InactivityHideTimeout; - InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; - InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary( - pair => pair.Key, - pair => pair.Value - ); - InactivityHideExtraChatAll = other.InactivityHideExtraChatAll; - InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet(); - ShowHideButton = other.ShowHideButton; - NativeItemTooltips = other.NativeItemTooltips; - ScreenshotMode = other.ScreenshotMode; - PrettierTimestamps = other.PrettierTimestamps; - MoreCompactPretty = other.MoreCompactPretty; - HideSameTimestamps = other.HideSameTimestamps; - ShowNoviceNetwork = other.ShowNoviceNetwork; - SidebarTabView = other.SidebarTabView; - PrintChangelog = other.PrintChangelog; - OnlyPreviewIf = other.OnlyPreviewIf; - PreviewMinimum = other.PreviewMinimum; - PreviewPosition = other.PreviewPosition; - CommandHelpSide = other.CommandHelpSide; - KeybindMode = other.KeybindMode; - LanguageOverride = other.LanguageOverride; - CanMove = other.CanMove; - CanResize = other.CanResize; - ShowTitleBar = other.ShowTitleBar; - ShowPopOutTitleBar = other.ShowPopOutTitleBar; - DatabaseBattleMessages = other.DatabaseBattleMessages; - FilterIncludePreviousSessions = other.FilterIncludePreviousSessions; - SortAutoTranslate = other.SortAutoTranslate; - CollapseDuplicateMessages = other.CollapseDuplicateMessages; - CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks; - SymbolPickerEnabled = other.SymbolPickerEnabled; - PlaySounds = other.PlaySounds; - CustomSoundVolume = other.CustomSoundVolume; - NotifyFailedTell = other.NotifyFailedTell; - NotifyPluginDisclosure = other.NotifyPluginDisclosure; - KeepInputFocus = other.KeepInputFocus; - MaxLinesToRender = other.MaxLinesToRender; - Use24HourClock = other.Use24HourClock; - ShowEmotes = other.ShowEmotes; - // Deep-copy so settings window edits don't leak into live config before Save. - BlockedEmotes = new HashSet(other.BlockedEmotes); - FontsEnabled = other.FontsEnabled; - ItalicEnabled = other.ItalicEnabled; - ExtraGlyphRanges = other.ExtraGlyphRanges; - FontSizeV2 = other.FontSizeV2; - GlobalFontV2 = other.GlobalFontV2; - JapaneseFontV2 = other.JapaneseFontV2; - ItalicFontV2 = other.ItalicFontV2; - SymbolsFontSizeV2 = other.SymbolsFontSizeV2; - TooltipOffset = other.TooltipOffset; - ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); - ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton; - - // Keep live temp tabs alive across UpdateFrom — a settings save must - // 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 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, t.CurrentChannel)); - - Tabs = other - .Tabs.Where(t => !t.IsTempTab || t.IsPinned) - .Select(t => - { - var clone = t.Clone(); - if (livePersistentSession.TryGetValue(clone.Identifier, out var live)) - { - clone.Messages = live.Messages; - clone.LastSendUnread = live.LastSendUnread; - clone.CurrentChannel = live.CurrentChannel; - } - return clone; - }) - .ToList(); - Tabs.AddRange(liveUnpinnedTempTabs); - - ChatTabForward = other.ChatTabForward; - ChatTabBackward = other.ChatTabBackward; - - PrivacyFilterEnabled = other.PrivacyFilterEnabled; - PrivacyPersistChannels = [.. other.PrivacyPersistChannels]; - PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels; - - RetentionEnabled = other.RetentionEnabled; - RetentionDefaultDays = other.RetentionDefaultDays; - RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary( - p => p.Key, - p => p.Value - ); - RetentionLastRunAt = other.RetentionLastRunAt; - - FirstRunCompleted = other.FirstRunCompleted; - WizardLastShownVersion = other.WizardLastShownVersion; - UseHellionFont = other.UseHellionFont; - ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader; - ShowHonorificGlow = other.ShowHonorificGlow; - - // v1.1.0 theme engine fields - Theme = other.Theme; - WindowOpacity = other.WindowOpacity; - WindowOpacityInactive = other.WindowOpacityInactive; - ReduceMotion = other.ReduceMotion; - UseCompactDensity = other.UseCompactDensity; - - EnableAutoTellTabs = other.EnableAutoTellTabs; - AutoTellTabsLimit = other.AutoTellTabsLimit; - AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; - AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; - SidebarWidth = other.SidebarWidth; - AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle; - - SeenPopOutInputHint = other.SeenPopOutInputHint; - PopOutInputEnabled = other.PopOutInputEnabled; - SeenPopOutHeaderHint = other.SeenPopOutHeaderHint; - AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout; - - WorldSuffixMode = other.WorldSuffixMode; - NameFormMode = other.NameFormMode; - - MainWindowOpen = other.MainWindowOpen; - SettingsWindowOpen = other.SettingsWindowOpen; - MaxParallelPopouts = other.MaxParallelPopouts; - TellAutoOpenMode = other.TellAutoOpenMode; - TellAutoOpenSwitchAlways = other.TellAutoOpenSwitchAlways; - SidebarAutoSwitchThresholdPx = other.SidebarAutoSwitchThresholdPx; - MainWindowLayoutMode = other.MainWindowLayoutMode; - } } [Serializable] @@ -540,9 +379,6 @@ public class Tab // Optional FontAwesome glyph name; null falls back to TabIconMapping default. public string? Icon = null; - [Obsolete("Removed in favor of SelectedChannels")] - public Dictionary ChatCodes = new(); - public Dictionary SelectedChannels = new(); public bool ExtraChatAll; public HashSet ExtraChatChannels = []; @@ -559,12 +395,15 @@ public class Tab public bool CanMove = true; public bool CanResize = true; - public bool IndependentHide; - public bool HideDuringCutscenes = true; - public bool HideWhenNotLoggedIn = true; - public bool HideWhenUiHidden = true; - public bool HideInLoadingScreens; - public bool HideInBattle; + // Six per-tab hide conditions used to live here. Their reader was the + // pop-out window, which stopped consulting them in cf4705e; the equivalents + // that survive are the window-level fields of the same name further up this + // file, and v1.12.0 gave every one of those a control. + // + // Per-tab was the wrong unit anyway: "hide during cutscenes" is a statement + // about the screen, not about one conversation. + // + // HideWhenInactive stays -- the auto-tell service writes it. public bool HideWhenInactive; public bool IsTempTab; @@ -667,15 +506,14 @@ public class Tab public Tab Clone() { -#pragma warning disable CS0618 // ChatCodes is obsolete but still serialized and must survive a clone return new Tab { Name = Name, - // Both were missing: Icon feeds the sidebar glyph, ChatCodes carries - // legacy migration data that is still written to the JSON. A clone - // round-trip used to drop them silently. + // Icon feeds the sidebar glyph and a clone round-trip used to drop + // it silently. ChatCodes sat beside it until v1.12.0, carrying data + // for a migration that the v16 schema gate had already made + // unreachable. Icon = Icon, - ChatCodes = new Dictionary(ChatCodes), SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value), ExtraChatAll = ExtraChatAll, ExtraChatChannels = ExtraChatChannels.ToHashSet(), @@ -693,12 +531,6 @@ public class Tab CurrentChannel = CurrentChannel.Clone(), CanMove = CanMove, CanResize = CanResize, - IndependentHide = IndependentHide, - HideDuringCutscenes = HideDuringCutscenes, - HideWhenNotLoggedIn = HideWhenNotLoggedIn, - HideWhenUiHidden = HideWhenUiHidden, - HideInLoadingScreens = HideInLoadingScreens, - HideInBattle = HideInBattle, HideWhenInactive = HideWhenInactive, IsTempTab = IsTempTab, IsPinned = IsPinned, @@ -708,7 +540,6 @@ public class Tab NotificationSoundId = NotificationSoundId, IsGreeted = IsGreeted, }; -#pragma warning restore CS0618 } /// Ordered message list with duplicate ID tracking, sorting and mutex protection. diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index a80bcbd..9e28c46 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -167,6 +167,14 @@ public sealed class Plugin : IAsyncDalamudPlugin // via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail // before they touch state that has already been torn down. Volatile // because the tick reads it from a different thread than the writer. + // The three hide conditions v1.5.6 evaluated and cf4705e left without a + // reader. Advanced once per draw, before any window is drawn. + private Util.ChatHideReason _hideReason = Util.ChatHideReason.None; + + // Set by the chat-activation keybind, consumed by the next hide evaluation. + // A cutscene the user dismissed stays dismissed until it ends. + internal bool ChatActivationRequested; + private volatile bool _isDisposing; // Read by background workers that outlive a teardown -- the export thread @@ -309,7 +317,13 @@ public sealed class Plugin : IAsyncDalamudPlugin ); } - Config.Version = 24; + // v25 carries no migration step. The schema gate above only refuses + // anything under 16, and Json.NET drops keys it does not recognise on + // load, so the fields v1.12.0 deleted simply stop being written on the + // next save. The bump is documentation, and it has to be consistent: + // the constant and this stamp are two separate places, and changing + // only one gives a config that re-stamps itself on every start. + Config.Version = 25; // Unpinned TempTabs are session-only and dropped on every load. Pinned // TempTabs survive reload — Jin's tester feedback (v1.4.7). @@ -470,7 +484,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.SettingsWindowOpenStep(this), new SelfTests.OnOpenMainUiRoutesMainWindowStep(this), new SelfTests.TypingIpcStateStep(this), - new SelfTests.ConfigMigrationV24Step(this), + new SelfTests.ConfigMigrationV25Step(this), new SelfTests.ChannelPopoutBindStep(this), new SelfTests.HoverStateFootprintStep(), new SelfTests.HonorificHeaderRenderStep(this), @@ -1203,6 +1217,32 @@ public sealed class Plugin : IAsyncDalamudPlugin Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden; + // Stateless, so it needs no machine: there is no gesture that shows + // the chat while nobody is logged in. + if (Config.HideWhenNotLoggedIn && !ClientState.IsLoggedIn) + { + TypingIpc.Update(); + return; + } + + _hideReason = Util.ChatHideState.Next( + _hideReason, + new Util.ChatHideState.Inputs( + Config.HideInBattle, + InBattle, + Config.HideDuringCutscenes, + CutsceneActive || GposeActive, + ChatActivationRequested + ) + ); + ChatActivationRequested = false; + + if (Util.ChatHideState.Hides(_hideReason)) + { + TypingIpc.Update(); + return; + } + // RegularFont is nullable only because the live rebuild path // disposes it before reassigning; both ends of that swap happen on // this same draw thread, so it cannot be null here. diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 7f57fef..c4b4a68 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -147,6 +147,7 @@ internal class HellionStrings internal static string Wizard_Step4_Summary_TellTabs => Get(nameof(Wizard_Step4_Summary_TellTabs)); internal static string Wizard_Step4_Summary_Visual => Get(nameof(Wizard_Step4_Summary_Visual)); internal static string Wizard_Step4_Summary_Unchanged => Get(nameof(Wizard_Step4_Summary_Unchanged)); + internal static string Wizard_Step4_Summary_Off => Get(nameof(Wizard_Step4_Summary_Off)); internal static string Wizard_Step4_TestHint => Get(nameof(Wizard_Step4_TestHint)); internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index c238ed5..dfbc04c 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -1214,4 +1214,7 @@ Insereix l'objecte enllaçat <item> + + desactivat + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index dca569f..4508fe2 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -1213,4 +1213,7 @@ Vložit odkázaný předmět <item> + + vypnuto + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index af39218..5ca8ba7 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -1213,4 +1213,7 @@ Indsæt linket genstand <item> + + fra + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index d86d3fa..2f657d5 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -1208,4 +1208,7 @@ Verlinkten Gegenstand einfügen <item> + + aus + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index d0d44c0..8b0c41c 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -1213,4 +1213,7 @@ Εισαγωγή συνδεδεμένου αντικειμένου <item> + + ανενεργό + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index b550565..d3b323d 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -1214,4 +1214,7 @@ Insertar objeto enlazado <item> + + desactivado + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index 4527a49..0d88537 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -1213,4 +1213,7 @@ Lisää linkitetty esine <item> + + pois + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index 3385228..ec917d2 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -1214,4 +1214,7 @@ Insérer l'objet lié <item> + + désactivé + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index 89231d5..7884c81 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -1213,4 +1213,7 @@ Hivatkozott tárgy beszúrása <item> + + kikapcsolva + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index e21a16f..ec73987 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -1214,4 +1214,7 @@ Inserisci l'oggetto collegato <item> + + disattivato + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index 99fa915..8a02c96 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -1214,4 +1214,7 @@ リンクしたアイテムを挿入 <item> + + オフ + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index 645788a..c07c4bf 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -1214,4 +1214,7 @@ 연결된 아이템 삽입 <item> + + 끔 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index c9e9ee5..43f0671 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -1213,4 +1213,7 @@ Sett inn lenket gjenstand <item> + + av + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index 215be2f..77f653c 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -1214,4 +1214,7 @@ Gekoppeld voorwerp invoegen <item> + + uit + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index 7d02ae7..5fa9e63 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -1213,4 +1213,7 @@ Wstaw powiązany przedmiot <item> + + wyłączone + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index d97fa64..8544c04 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -1214,4 +1214,7 @@ Inserir item vinculado <item> + + desativado + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 9e9813b..40c813b 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -1213,4 +1213,7 @@ Inserir item ligado <item> + + desativado + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 5e4e12f..9d91d8c 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1225,4 +1225,7 @@ The game reports a failed tell in the log only. This raises a notification instead, so a message to someone offline or on another world does not go unnoticed. + + off + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index eed5fcb..66138da 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -1214,4 +1214,7 @@ Inserează obiectul legat <item> + + dezactivat + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index 96e5a9b..097e003 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -1214,4 +1214,7 @@ Вставить связанный предмет <item> + + выключено + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index f563572..7576dea 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -1214,4 +1214,7 @@ Infoga länkat föremål <item> + + av + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index 14c9e78..c8d2658 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -1213,4 +1213,7 @@ Bağlantılı eşyayı ekle <item> + + kapalı + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 2a49c92..8ac78e7 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -1213,4 +1213,7 @@ Вставити пов'язаний предмет <item> + + вимкнено + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index 0264a9c..95890cd 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -1214,4 +1214,7 @@ 插入关联物品 <item> + + 关闭 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index 5f84eaf..4dbda0a 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -1214,4 +1214,7 @@ 插入關聯物品 <item> + + 關閉 + \ No newline at end of file diff --git a/HellionChat/SelfTests/ConfigMigrationV24Step.cs b/HellionChat/SelfTests/ConfigMigrationV25Step.cs similarity index 92% rename from HellionChat/SelfTests/ConfigMigrationV24Step.cs rename to HellionChat/SelfTests/ConfigMigrationV25Step.cs index 1d44dee..3219a13 100644 --- a/HellionChat/SelfTests/ConfigMigrationV24Step.cs +++ b/HellionChat/SelfTests/ConfigMigrationV25Step.cs @@ -8,20 +8,20 @@ namespace HellionChat.SelfTests; // below must carry valid values here. This probe never rewrites config; the // migrations themselves are load-time and verified by the prepared-config smoke // in the plan. -internal sealed class ConfigMigrationV24Step : ISelfTestStep +internal sealed class ConfigMigrationV25Step : ISelfTestStep { - public ConfigMigrationV24Step(Plugin plugin) + public ConfigMigrationV25Step(Plugin plugin) { _ = plugin; } - public string Name => "Hellion Chat - Config v24 migration"; + public string Name => "Hellion Chat - Config v25 migration"; public SelfTestStepResult RunStep() { - if (Plugin.Config.Version != 24) + if (Plugin.Config.Version != 25) { - ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 24"); + ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 25"); return SelfTestStepResult.Fail; } diff --git a/HellionChat/Ui/AutoTellTabTint.cs b/HellionChat/Ui/AutoTellTabTint.cs new file mode 100644 index 0000000..70035c3 --- /dev/null +++ b/HellionChat/Ui/AutoTellTabTint.cs @@ -0,0 +1,97 @@ +namespace HellionChat.Ui; + +// Deterministic hash-based color and icon tinting for Auto-Tell sidebar tabs. +// Same tell partner (name+world) always produces the same color and icon across +// sessions. Pure string logic, no Dalamud dependency — testable without game refs. +internal static class AutoTellTabTint +{ + // Fallback for invalid input (empty name or world=0). White matches + // TextPrimary default so the sidebar stays visually consistent. + public const uint Fallback = 0xFFFFFFFFu; + + // 12 saturated mid-bright colors from the built-in theme pool, readable + // on dark backgrounds. Collision risk is low at realistic 1-5 active tells. + // RGBA format, matching ColourUtil.RgbaToAbgr convention. + public static readonly IReadOnlyList Palette = new uint[] + { + 0x00BED2FFu, // Arctic Cyan + 0xF97316FFu, // Ember Orange + 0xB585FFFFu, // Light Cosmic Purple + 0xE374E8FFu, // Bloom Magenta + 0x5DD39EFFu, // Mint Green + 0xF0AD4EFFu, // Warning Yellow + 0xE85C6AFFu, // Coral + 0x5CB85CFFu, // Status Green + 0x6278FFFFu, // Bloom Blue + 0xC9982EFFu, // Warm Gold + 0x9CCB7CFFu, // Soft Sage + 0xE85D04FFu, // Deep Ember + }; + + public static uint For(string name, uint world) + { + if (string.IsNullOrEmpty(name) || world == 0) + return Fallback; + + return Palette[(int)(StableHash($"{name}@{world}") % Palette.Count)]; + } + + // 7 visually distinct FA glyphs that make sense in a tell context. + // Excludes cog/comment/users — those read as system or group tabs. + public static readonly IReadOnlyList IconPool = new[] + { + "envelope", + "star", + "heart", + "bell", + "bookmark", + "flag", + "fire", + }; + + // "envelope" matches the tell context better than the old hardcoded "clock". + public const string IconFallback = "envelope"; + + public static string IconFor(string name, uint world) + { + if (string.IsNullOrEmpty(name) || world == 0) + return IconFallback; + + // Reversed key ("world@name") gives icon and color independent variation + // so the same tell partner doesn't always get the same color+icon pair. + // 7 icons x 12 colors = 84 distinct combinations. + return IconPool[(int)(StableHash($"{world}@{name}") % IconPool.Count)]; + } + + // FNV-1a, not string.GetHashCode. The header of this file promises the same + // partner produces the same colour "across sessions", and GetHashCode cannot + // keep that: .NET salts string hashing per process, so every game start + // would reshuffle every tell tab. The tests never caught it because they + // only ever compared two calls inside one run. + // Returns the full uint. The old int-based version masked off the sign bit + // before its modulo; on a uint that mask only throws away a bit of entropy. + private static uint StableHash(string key) + { + const uint offsetBasis = 2166136261u; + const uint prime = 16777619u; + + var hash = offsetBasis; + foreach (var b in System.Text.Encoding.UTF8.GetBytes(key)) + { + hash ^= b; + hash *= prime; + } + + // Avalanche step, and not optional. FNV-1a alone leaves the low bits + // correlated for keys that differ only slightly, and the caller takes + // exactly those bits with a modulo -- a probe over 144 near-identical + // keys reached only 6 of the 12 colours. With fmix32 it reaches all 12. + hash ^= hash >> 16; + hash *= 0x7feb352du; + hash ^= hash >> 15; + hash *= 0x846ca68bu; + hash ^= hash >> 16; + + return hash; + } +} diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index 67982eb..e9fc90a 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -37,9 +37,10 @@ internal sealed class MessageList private readonly Action _drawCompactRow; private readonly Action _drawCardRow; - // Reused across frames: at the default MaxLinesToRender of 2500 a fresh - // array per frame is 10 KB of garbage, and A2 put the default density on - // this path. + // Reused across frames: at MessageManager.MessageDisplayLimit a fresh array + // per frame is 40 KB of garbage, and A2 put the default density on this + // path. The old comment named MaxLinesToRender and its 2500 default, a + // config field that had stopped bounding anything. private float[] _heightScratch = []; // §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle. diff --git a/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs b/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs index 03849cc..4ca54ba 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs @@ -121,7 +121,13 @@ internal sealed class WindowTab ); } - if (_w.Section(ImGui.GetID("window.preview"u8), "Input preview", open: false)) + if ( + _w.Section( + ImGui.GetID("window.preview"u8), + HellionStrings.Settings_Section_InputPreview, + open: false + ) + ) { _w.EnumComboRow( ImGui.GetID("window.preview.position"u8), @@ -186,6 +192,31 @@ internal sealed class WindowTab () => Plugin.Config.HideInNewGamePlusMenu, v => Plugin.Config.HideInNewGamePlusMenu = v ); + + // The three that lost their reader with the chat window. Pressing + // the chat key during a cutscene brings the chat back for that + // cutscene; combat has no such escape, exactly as in v1.5.6. + _w.ToggleRow( + ImGui.GetID("window.hide.cutscenes"u8), + Language.Options_HideDuringCutscenes_Name, + Language.Options_HideDuringCutscenes_Description, + () => Plugin.Config.HideDuringCutscenes, + v => Plugin.Config.HideDuringCutscenes = v + ); + _w.ToggleRow( + ImGui.GetID("window.hide.battle"u8), + Language.Options_HideInBattle_Name, + Language.Options_HideInBattle_Description, + () => Plugin.Config.HideInBattle, + v => Plugin.Config.HideInBattle = v + ); + _w.ToggleRow( + ImGui.GetID("window.hide.notloggedin"u8), + Language.Options_HideWhenNotLoggedIn_Name, + Language.Options_HideWhenNotLoggedIn_Description, + () => Plugin.Config.HideWhenNotLoggedIn, + v => Plugin.Config.HideWhenNotLoggedIn = v + ); } if ( diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 179eaa5..b0b4a02 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -324,10 +324,17 @@ internal sealed class Sidebar var icon = ResolveTabIcon(tab); - // Dim precedence (1.5.6): the active tab always keeps its regular - // color; only greeted, non-active tabs drop to TextDim. + // Dim precedence (1.5.6): the active tab never dims; only greeted, + // non-active tabs drop to TextDim. "Regular colour" is the tint for an + // auto-tell tab and the theme text colour for everything else. + // + // Below that, an auto-tell tab is tinted from its partner. Twelve + // colours against seven glyphs is 84 combinations, which is plenty for + // the one to five conversations anybody actually runs in parallel. The + // greeted dim still wins: it says something about this tab right now, + // the tint only says who it belongs to. var isCurrentTab = tab == activeTab; - var iconColor = textAbgr; + var iconColor = tab.IsTempTab ? ColourUtil.RgbaToAbgr(TabTintCache.GetTint(tab)) : textAbgr; if ( !isCurrentTab && greetedConfigured @@ -486,10 +493,20 @@ internal sealed class Sidebar ) return mapped; - // Auto-tell tabs always show the envelope, regardless of what their - // SelectedChannels filter is set to. + // Auto-tell tabs get one of seven glyphs derived from the partner, not + // one envelope for all of them. With four tells open, identical rows in + // identical colour are four rows you have to read to tell apart. if (tab.IsTempTab) - return FontAwesomeIcon.Envelope; + { + // TryGetValue rather than an indexer: every glyph in the pool is in + // the table today, and a lookup miss would be somebody editing one + // of the two lists without the other. A wrong envelope beats a + // KeyNotFoundException on the draw thread. + var hashed = TabTintCache.GetIcon(tab); + return IconByName.TryGetValue(hashed, out var tellGlyph) + ? tellGlyph + : FontAwesomeIcon.Envelope; + } // Channel-type fallback. Walk every selected key, not just the first, // so a System tab that filters multiple system-flavoured ChatTypes diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index 240badf..ea2017d 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -483,10 +483,16 @@ public sealed class FirstRunWizard : Window string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel) ); - var historyLabel = - (_state.PendingFilterIncludePreviousSessions ?? false) - ? HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label - : HellionStrings.Wizard_Step4_Summary_Unchanged; + // Three states, not two. "Unchanged" is for a step the user + // never entered; an unticked box is a decision that gets + // committed, and reporting it as unchanged was the same kind of + // lie the deleted LoadPreviousSession told. + var historyLabel = _state.PendingFilterIncludePreviousSessions switch + { + true => HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, + false => HellionStrings.Wizard_Step4_Summary_Off, + null => HellionStrings.Wizard_Step4_Summary_Unchanged, + }; ImGui.TextWrapped( string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel) ); diff --git a/HellionChat/Ui/TabTintCache.cs b/HellionChat/Ui/TabTintCache.cs new file mode 100644 index 0000000..5364ca4 --- /dev/null +++ b/HellionChat/Ui/TabTintCache.cs @@ -0,0 +1,38 @@ +namespace HellionChat.Ui; + +// Per-Tab cache wrapper around the pure AutoTellTabTint hash helpers. +// Each cache (tint, icon) carries its own name+world validation key so +// neither read path mutates the other's state — refilling one never +// invalidates the other. No string allocation in the steady-state lookup. +internal static class TabTintCache +{ + public static uint GetTint(Tab tab) + { + var name = tab.TellTarget.Name; + var world = tab.TellTarget.World; + if (tab._cachedTintTellName != name || tab._cachedTintTellWorld != world) + { + tab._cachedTintTellName = name; + tab._cachedTintTellWorld = world; + tab._cachedTellTint = AutoTellTabTint.For(name, world); + } + return tab._cachedTellTint; + } + + public static string GetIcon(Tab tab) + { + var name = tab.TellTarget.Name; + var world = tab.TellTarget.World; + if ( + tab._cachedTellIcon is null + || tab._cachedIconTellName != name + || tab._cachedIconTellWorld != world + ) + { + tab._cachedIconTellName = name; + tab._cachedIconTellWorld = world; + tab._cachedTellIcon = AutoTellTabTint.IconFor(name, world); + } + return tab._cachedTellIcon; + } +} diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 737d73c..9684f62 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -226,6 +226,10 @@ internal sealed class MainWindow : Window, IFocusableChatWindow internal void ActivateChat() { _userHidden = false; + + // Also lifts a cutscene hide for the duration of that cutscene. Without + // this the key would appear to do nothing at all during one. + Plugin.Instance.ChatActivationRequested = true; if (!IsOpen) { IsOpen = true; diff --git a/HellionChat/Util/ChatHideState.cs b/HellionChat/Util/ChatHideState.cs new file mode 100644 index 0000000..1c99b88 --- /dev/null +++ b/HellionChat/Util/ChatHideState.cs @@ -0,0 +1,76 @@ +namespace HellionChat.Util; + +// Why the chat is currently hidden, if it is. +internal enum ChatHideReason +{ + None, + + // In combat, and the user asked for that to hide the chat. + Battle, + + // A cutscene or gpose is running. + Cutscene, + + // A cutscene is running and the user pressed the activation key anyway. + // Distinct from None so the state returns to hidden on its own once the + // cutscene ends, without a second gesture. + CutsceneOverride, +} + +// The hide conditions v1.5.6 evaluated every frame, as a state machine. +// +// Three of the four went missing with the chat window in cf4705e: their config +// fields survived, their translated labels survived, nothing read them. They +// need a machine rather than three ifs because two of them are not conditions +// but states: a cutscene the user has dismissed must stay dismissed until the +// cutscene ends, and combat that started while the chat was already hidden for +// another reason must not take ownership of it. +// +// Pure and Dalamud-free, so the transitions can be pinned without a game. +internal static class ChatHideState +{ + internal readonly record struct Inputs( + bool HideInBattle, + bool InBattle, + bool HideDuringCutscenes, + bool CutsceneActive, + bool ActivateRequested + ); + + internal static ChatHideReason Next(ChatHideReason current, in Inputs inputs) + { + // Leaving a state comes first. Otherwise a frame in which combat ends + // and a cutscene starts would keep reporting Battle. + if (current == ChatHideReason.Battle && !inputs.InBattle) + current = ChatHideReason.None; + + if ( + current is ChatHideReason.Cutscene or ChatHideReason.CutsceneOverride + && !inputs.CutsceneActive + ) + current = ChatHideReason.None; + + // The user asked for the chat during a cutscene. Held until the cutscene + // ends, which the branch above takes care of. + if (current == ChatHideReason.Cutscene && inputs.ActivateRequested) + return ChatHideReason.CutsceneOverride; + + if (current != ChatHideReason.None) + return current; + + // Entering. Cutscene wins over battle: a cutscene during combat is the + // more specific situation, and it is the one with an escape hatch. + if (inputs.HideDuringCutscenes && inputs.CutsceneActive) + return ChatHideReason.Cutscene; + + if (inputs.HideInBattle && inputs.InBattle) + return ChatHideReason.Battle; + + return ChatHideReason.None; + } + + // CutsceneOverride is a hide state that does not hide -- that is the whole + // point of it. + internal static bool Hides(ChatHideReason reason) => + reason is ChatHideReason.Battle or ChatHideReason.Cutscene; +} diff --git a/HellionChat/Util/LayoutFingerprint.cs b/HellionChat/Util/LayoutFingerprint.cs index 22052ba..f6c6785 100644 --- a/HellionChat/Util/LayoutFingerprint.cs +++ b/HellionChat/Util/LayoutFingerprint.cs @@ -21,8 +21,8 @@ internal readonly record struct LayoutFingerprint( // Dragging a window edge or the Dalamud UI-scale slider moves the continuous // half of the fingerprint on every frame. Acting on each one drops the height // cache and sends the whole tab through the linear measure path (up to -// Config.MaxLinesToRender rows). Waiting for those to settle turns a drag into -// one rebuild. Discrete changes bypass the wait entirely. +// MessageManager.MessageDisplayLimit rows). Waiting for those to settle turns a +// drag into one rebuild. Discrete changes bypass the wait entirely. internal sealed class LayoutFingerprintGate { internal const long SettleMs = 200; diff --git a/HellionChat/Util/TabLifecycleHelpers.cs b/HellionChat/Util/TabLifecycleHelpers.cs index fb0a4f3..093eec4 100644 --- a/HellionChat/Util/TabLifecycleHelpers.cs +++ b/HellionChat/Util/TabLifecycleHelpers.cs @@ -22,7 +22,7 @@ internal static class TabLifecycleHelpers // with no window. Unconditional (pinned included) because pinned TempTabs // survive the load and are the main stale-flag source; a !IsPinned filter // would leave exactly those leaking. Lockstep with the two in-memory resets - // (AutoTellTabsService pool-full + Configuration.UpdateFrom backToOriginal). + // (AutoTellTabsService pool-full + the settings round trip backToOriginal). // TEST-MIRROR: ../../../Hellion Build test/_Helpers/PopOutResetOnLoadTests.cs internal static void ResetPopOutOnLoad(IEnumerable tabs) {