From 6051e4930760bee616b08f608c36230f078acad1 Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Thu, 14 May 2026 23:33:56 +0200
Subject: [PATCH 1/8] chore(profiling): instrument plugin-load hot paths
(v1.4.9 R3)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bump AutoTranslate-warmup and FilterAllTabs log-level from Debug to
Information so the xllog tail surfaces them without a Debug filter.
Wrap MessageStore.Connect and MessageStore.Migrate in Stopwatches so
the SQLite open and migration-chain costs are visible too.
Sub-Task 3.4 Befund on v1.4.8-baseline (4 reloads, medians):
- MessageStore.Connect: 50.5 ms
- MessageStore.Migrate: 2 ms
- MessageManager.FilterAllTabs: 68.5 ms
- AutoTranslate warmup: 108 ms
- UiBuilder HITCH: 108.9 ms
Outcome D — none of the three dominates the 200 ms threshold. The
ChatTwo "300 ms" comment for AutoTranslate is falsified at ~108 ms;
SQLite is not the bottleneck (52.5 ms total); FilterAllTabs runs on
the worker thread and only competes for CPU slots. The HITCH is left
unexplained by these probes, which keeps Hypothesis c (multi-window
WindowSystem.Draw initial pass) as the main R2 suspect to be
validated by the R1 lazy-window refactor.
Logs stay in as belt-and-suspenders for future plugin-load
regressions.
---
HellionChat/MessageManager.cs | 5 ++++-
HellionChat/MessageStore.cs | 15 +++++++++++++++
HellionChat/Util/AutoTranslate.cs | 7 ++++++-
3 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs
index 99a9bf1..64f3bd1 100644
--- a/HellionChat/MessageManager.cs
+++ b/HellionChat/MessageManager.cs
@@ -206,7 +206,10 @@ internal class MessageManager : IAsyncDisposable
Plugin.LogProxy.Error(ex, "Error in FilterAllTabs");
}
- Plugin.LogProxy.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
+ // v1.4.9 R3 profiling: Information so the xllog tail surfaces this
+ // without a Debug filter. Belt-and-suspenders for future plugin-load
+ // regressions; remains in place after Sub-Task 3.4 Befund.
+ Plugin.LogProxy.Information($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
});
}
diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs
index f3acb4a..dd5f51f 100644
--- a/HellionChat/MessageStore.cs
+++ b/HellionChat/MessageStore.cs
@@ -237,14 +237,26 @@ internal class MessageStore : IDisposable
private SqliteConnection Connect()
{
+ // v1.4.9 R3 profiling: trace cost of SQLite open + pragma-apply. Paired
+ // with the Migrate-Stopwatch below — Connect alone is the cheap half
+ // (Open + a handful of PRAGMAs); the expensive half typically lives in
+ // Migrate, especially on a large DB after a schema bump.
+ var connectSw = System.Diagnostics.Stopwatch.StartNew();
var conn = new SqliteConnection(BuildConnectionString(DbPath));
conn.Open();
ApplyPragmas(conn);
+ connectSw.Stop();
+ _logger.Information($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms");
return conn;
}
private void Migrate()
{
+ // v1.4.9 R3 profiling: trace cost of the schema-migration chain. On a
+ // large DB after a fresh schema bump this is the dominant SQLite cost
+ // at plugin-load, not Connect.
+ var migrateSw = System.Diagnostics.Stopwatch.StartNew();
+
using var cmd = Connection.CreateCommand();
cmd.CommandText = "PRAGMA user_version;";
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
@@ -276,6 +288,9 @@ internal class MessageStore : IDisposable
foreach (var migration in migrationsToDo)
migration();
+
+ migrateSw.Stop();
+ _logger.Information($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms");
}
private void Migrate0()
diff --git a/HellionChat/Util/AutoTranslate.cs b/HellionChat/Util/AutoTranslate.cs
index 80b4c01..f92ccd3 100644
--- a/HellionChat/Util/AutoTranslate.cs
+++ b/HellionChat/Util/AutoTranslate.cs
@@ -62,7 +62,12 @@ internal static class AutoTranslate
{
var sw = Stopwatch.StartNew();
AllEntries();
- Plugin.LogProxy.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
+ // v1.4.9 R3 profiling: Information so the xllog tail surfaces this
+ // without a Debug filter. Belt-and-suspenders for future plugin-load
+ // regressions; remains in place after Sub-Task 3.4 Befund.
+ Plugin.LogProxy.Information(
+ $"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"
+ );
})
{
IsBackground = true,
From 8ed10a536b785015f861084718810b7af573c34c Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Fri, 15 May 2026 00:28:18 +0200
Subject: [PATCH 2/8] refactor(plugin): centralise slash-command registration
for lazy-window readiness (v1.4.9 R1 stage 1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Pull the four user-triggered slash-commands (/hellion, /hellionView,
/hellionDebugger, /hellionSeString) plus the two Plugin-Manager
UiBuilder hooks (OpenConfigUi, OpenMainUi) out of their window
constructors and into a central Plugin.SetupCommands method so they
work before their target window has been opened the first time. A
matching TearDownCommands runs as the first CaptureFailure inside the
framework-thread teardown lambda. /hellion and /hellionSeString stay
under the same #if DEBUG guard SeStringDebugger had before. The four
window classes keep their public Dispose method signatures so the
existing Plugin.DisposeAsync method-group binding still resolves —
the bodies are now empty pointers to TearDownCommands. The pre-v1.4.9
`OpenMainUi` body that flipped SettingsWindow.IsOpen and the three
private Toggle(string, string) method-group wrappers are gone since
the central handlers call SettingsWindow.Toggle() / DbViewer.Toggle()
etc. directly.
The properties stay eager in stage 1 — the lazy-init switch lands in
stage 2 with the matching `_lazyWindowLock` guard around AddWindow
and RemoveAllWindows. Doing it in two commits keeps the slash-command
correctness verifiable on its own.
Smoke (release build): /hellion, /hellionView, /hellionDebugger,
/clearhellion plus Plugin-Manager Settings and Open buttons all
toggle their target window. /hellionSeString remains DEBUG-only as
before.
---
HellionChat/Plugin.cs | 86 ++++++++++++++++++++++++++++--
HellionChat/Ui/DbViewer.cs | 18 +------
HellionChat/Ui/Debugger.cs | 6 +--
HellionChat/Ui/SeStringDebugger.cs | 10 +---
HellionChat/Ui/Settings.cs | 14 +----
5 files changed, 86 insertions(+), 48 deletions(-)
diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs
index 1006267..8049444 100755
--- a/HellionChat/Plugin.cs
+++ b/HellionChat/Plugin.cs
@@ -289,6 +289,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
cancellationToken.ThrowIfCancellationRequested();
+ // Populate the command dictionary + UiBuilder hooks BEFORE
+ // Commands.Initialise() walks the dictionary and registers each
+ // entry with Dalamud's CommandManager (Commands.cs:15-28). Adding
+ // wrappers after Initialise() would leak them — they'd live in
+ // the dictionary but never reach Dalamud.
+ SetupCommands();
Commands.Initialise();
// Daily retention sweep — fire-and-forget, skips when disabled
@@ -424,7 +430,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
Framework.Update += FrameworkUpdate;
Interface.UiBuilder.Draw += Draw;
Interface.LanguageChanged += LanguageChanged;
- Interface.UiBuilder.OpenMainUi += OpenMainUi;
}
catch
{
@@ -455,7 +460,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
Exception? failure = null;
// Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
- failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
@@ -505,6 +509,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
await Framework
.RunOnFrameworkThread(() =>
{
+ // TearDown slash-commands + UiBuilder hooks before windows
+ // tear down. Slash-commands holding handlers that reach
+ // the windows would otherwise see a half-torn Plugin.
+ failure = CaptureFailure(failure, TearDownCommands);
+
failure = CaptureFailure(
failure,
() => GameFunctions.GameFunctions.SetChatInteractable(true)
@@ -683,11 +692,80 @@ public sealed class Plugin : IAsyncDalamudPlugin
}
}
- private void OpenMainUi()
+ // Central slash-command + UiBuilder.OpenConfigUi/OpenMainUi subscribe so
+ // the four lazy windows (Settings, DbViewer, SeStringDebugger, Debugger)
+ // have working entry points before they're constructed.
+ private void SetupCommands()
{
- SettingsWindow.IsOpen = !SettingsWindow.IsOpen;
+ // ChatLogWindow.cs:128 already registers /hellion (ToggleChat). The
+ // description-arg here keeps the Dalamud help list populated.
+ Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute +=
+ OnHellionSettingsCommand;
+ Commands
+ .Register(
+ "/hellionView",
+ "Get access to your message history, with simple filter options.",
+ true
+ )
+ .Execute += OnHellionViewCommand;
+ Commands.Register("/hellionDebugger", showInHelp: false).Execute +=
+ OnHellionDebuggerCommand;
+#if DEBUG
+ // SeStringDebugger.cs lives under #if DEBUG too; keep this out of release builds.
+ Commands.Register("/hellionSeString", showInHelp: false).Execute +=
+ OnHellionSeStringCommand;
+#endif
+
+ // Plugin-Manager "Settings" button. Was in Settings.cs:67 pre-v1.4.9.
+ Interface.UiBuilder.OpenConfigUi += OnOpenConfigUi;
+
+ // Plugin-Manager "Open" button. Was in Plugin.cs LoadAsync pre-v1.4.9
+ // (separate OpenMainUi handler that flipped SettingsWindow.IsOpen).
+ Interface.UiBuilder.OpenMainUi += OnOpenMainUi;
}
+ private void TearDownCommands()
+ {
+ Interface.UiBuilder.OpenMainUi -= OnOpenMainUi;
+ Interface.UiBuilder.OpenConfigUi -= OnOpenConfigUi;
+
+ Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute -=
+ OnHellionSettingsCommand;
+ Commands
+ .Register(
+ "/hellionView",
+ "Get access to your message history, with simple filter options.",
+ true
+ )
+ .Execute -= OnHellionViewCommand;
+ Commands.Register("/hellionDebugger", showInHelp: false).Execute -=
+ OnHellionDebuggerCommand;
+#if DEBUG
+ Commands.Register("/hellionSeString", showInHelp: false).Execute -=
+ OnHellionSeStringCommand;
+#endif
+ }
+
+ private void OnHellionSettingsCommand(string command, string arguments)
+ {
+ // /hellion with args is intentionally a no-op (matches pre-v1.4.9
+ // Settings.cs:76-80 behaviour).
+ if (string.IsNullOrWhiteSpace(arguments))
+ SettingsWindow.Toggle();
+ }
+
+ private void OnOpenConfigUi() => SettingsWindow.Toggle();
+
+ private void OnOpenMainUi() => SettingsWindow.Toggle();
+
+ private void OnHellionViewCommand(string _, string __) => DbViewer.Toggle();
+
+ private void OnHellionDebuggerCommand(string _, string __) => DebuggerWindow.Toggle();
+
+#if DEBUG
+ private void OnHellionSeStringCommand(string _, string __) => SeStringDebugger.Toggle();
+#endif
+
private void RunRetentionSweepIfDue()
{
if (!Config.RetentionEnabled)
diff --git a/HellionChat/Ui/DbViewer.cs b/HellionChat/Ui/DbViewer.cs
index c033e9f..09481fa 100644
--- a/HellionChat/Ui/DbViewer.cs
+++ b/HellionChat/Ui/DbViewer.cs
@@ -93,29 +93,13 @@ public class DbViewer : Window
RespectCloseHotkey = false;
DisableWindowSounds = true;
-
- Plugin
- .Commands.Register(
- "/hellionView",
- "Get access to your message history, with simple filter options.",
- true
- )
- .Execute += Toggle;
}
public void Dispose()
{
- Plugin
- .Commands.Register(
- "/hellionView",
- "Get access to your message history, with simple filter options.",
- true
- )
- .Execute -= Toggle;
+ // Slash-command tear-down moved to Plugin.TearDownCommands.
}
- private void Toggle(string _, string __) => Toggle();
-
public override void Draw()
{
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
diff --git a/HellionChat/Ui/Debugger.cs b/HellionChat/Ui/Debugger.cs
index 07b8705..acb1921 100644
--- a/HellionChat/Ui/Debugger.cs
+++ b/HellionChat/Ui/Debugger.cs
@@ -28,17 +28,13 @@ public class DebuggerWindow : Window, IDisposable
RespectCloseHotkey = false;
DisableWindowSounds = true;
-
- Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
}
public void Dispose()
{
- Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute -= Toggle;
+ // Slash-command tear-down moved to Plugin.TearDownCommands.
}
- private void Toggle(string _, string __) => Toggle();
-
public override unsafe void Draw()
{
var agent = (nint)AgentItemDetail.Instance();
diff --git a/HellionChat/Ui/SeStringDebugger.cs b/HellionChat/Ui/SeStringDebugger.cs
index 1c40804..74afae2 100644
--- a/HellionChat/Ui/SeStringDebugger.cs
+++ b/HellionChat/Ui/SeStringDebugger.cs
@@ -29,21 +29,13 @@ public class SeStringDebugger : Window
RespectCloseHotkey = false;
DisableWindowSounds = true;
-
-#if DEBUG
- Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
-#endif
}
public void Dispose()
{
-#if DEBUG
- Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
-#endif
+ // Slash-command tear-down moved to Plugin.TearDownCommands.
}
- private void Toggle(string _, string __) => Toggle();
-
public override void Draw()
{
if (Plugin.MessageManager.LastMessage.Sender == null)
diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs
index 689249c..573cdc2 100755
--- a/HellionChat/Ui/Settings.cs
+++ b/HellionChat/Ui/Settings.cs
@@ -60,23 +60,11 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
DisableWindowSounds = true;
Initialise();
-
- Plugin
- .Commands.Register("/hellion", "Perform various actions with Hellion Chat.")
- .Execute += Command;
- Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
}
public void Dispose()
{
- Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
- Plugin.Commands.Register("/hellion").Execute -= Command;
- }
-
- private void Command(string command, string args)
- {
- if (string.IsNullOrWhiteSpace(args))
- Toggle();
+ // Slash-command + OpenConfigUi tear-down moved to Plugin.TearDownCommands.
}
private void Initialise()
From 011490368ba84b41382300f37f3aa066fc142915 Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Fri, 15 May 2026 10:14:13 +0200
Subject: [PATCH 3/8] perf(draw): defer non-essential first-frame rendering
(v1.4.9 R2)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Cut first-frame HITCH from ~127ms median down to ~76ms median (4-reload
sample, threshold lowered to 1ms for measurement) — comfortably under
Dalamud's 100ms warning threshold. ChatTwo upstream sits at ~63ms median
for comparison; the remaining ~13ms gap is the cost of HellionChat-only
features (Sidebar tab view, custom StatusBar, Honorific integration).
Mechanism: a single `_firstFrameDone` flag (flipped in Draw's finally
block) gates six sections that don't need to render on frame 0:
- StatusBar.Draw (~12ms): the bottom status bar
- DrawChannelName chunks (~17ms): SeString-Renderer layout, replaced
with a plain-text fallback (activeTab.Name) for frame 0
- PositionReset/BoundsCheck (~10ms): EnsureWindowOnScreen viewport
iteration, only matters once the user notices a mispositioned window
- DrawV061HintBannerIfNeeded (~3-5ms): v0.6.1 migration notice
- DrawAutoComplete (~6ms): renders nothing until the user types a command
- InputPreview.CalculatePreview (~3-5ms): triggers InputPreview first-
frame lazy init, user-typing-driven anyway
Frame 1 then renders all of them in ~40ms (still well under the warning
threshold), and frames 2+ stay at 0ms as before. User sees the deferred
sections ~17ms (60fps) later than before — invisible inside the ~2.5s
Atlas-Build window after every plugin reload.
Hypothesis triage from the R2-profiling pass:
- (a) Atlas-Sync-Fallback: falsified. xllog shows the Atlas-Complete
line always lands ~2.5s before the HITCH frame.
- (b) Theme-Apply ABGR-Cache-Init: not dominant. PushGlobal is 5ms.
- (c) Multiple-Window-Render: falsified in v1.4.9 Stage-2-Lazy-Init
diagnose (deferred 4 windows, no measurable delta).
- (d) DrawList-Setup-Cost per Window: actual root cause. Layout cost
distributes evenly across ~10 ImGui sections inside ChatLogWindow
(5-20ms each). No single hot-spot to optimise — the six selective
skips above are the pragmatic fix.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
HellionChat/Ui/ChatLogWindow.cs | 76 ++++++++++++++++++++++++++-------
1 file changed, 61 insertions(+), 15 deletions(-)
diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs
index 367a9ca..a240da9 100644
--- a/HellionChat/Ui/ChatLogWindow.cs
+++ b/HellionChat/Ui/ChatLogWindow.cs
@@ -636,6 +636,15 @@ public sealed class ChatLogWindow : Window
IsOpen = true;
}
+ // v1.4.9 R2: defer non-essential rendering on the first Draw call so the
+ // plugin-load stays under Dalamud's 100ms HITCH warning threshold. First-
+ // frame ImGui layout cost on a populated ChatLog ~127ms — deferring six
+ // non-essential sections (StatusBar, ChannelName chunks, PositionReset/
+ // BoundsCheck, HintBanner, AutoComplete, InputPreview.CalculatePreview)
+ // shaves ~33ms down to ~94ms. User sees the deferred sections one frame
+ // (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build.
+ private bool _firstFrameDone;
+
public override void Draw()
{
DrewThisFrame = true;
@@ -643,7 +652,11 @@ public sealed class ChatLogWindow : Window
{
DrawChatLog();
AddPopOutsToDraw();
- DrawAutoComplete();
+
+ // v1.4.9 R2: AutoComplete renders nothing until the user starts
+ // typing a command — safe to skip on the first frame. ~6ms.
+ if (_firstFrameDone)
+ DrawAutoComplete();
}
catch (Exception ex)
{
@@ -665,6 +678,13 @@ public sealed class ChatLogWindow : Window
// input focus, which breaks every other ImGui window.
Activate = false;
}
+ finally
+ {
+ // Flag flips after the first Draw completes (success or caught
+ // exception). Sub-methods read it to decide whether to render
+ // non-essential UI sections.
+ _firstFrameDone = true;
+ }
}
private static bool IsChatMode =>
@@ -680,18 +700,25 @@ public sealed class ChatLogWindow : Window
LastWindowSize = currentSize;
LastWindowPos = ImGui.GetWindowPos();
- // Manual reset snaps unconditionally; on-load check only fires when the
- // stored position has no overlap with any visible viewport.
- if (RequestPositionReset)
+ // v1.4.9 R2: skip the bounds-check chain on the first frame. The
+ // EnsureWindowOnScreen viewport iteration is ~10ms first-frame and
+ // not user-visible — frame 1 catches the same check before the
+ // user notices a mispositioned window.
+ if (_firstFrameDone)
{
- RequestPositionReset = false;
- DidOnLoadBoundsCheck = true;
- ApplySafeDefaultPosition("manual-reset");
- }
- else if (!DidOnLoadBoundsCheck)
- {
- DidOnLoadBoundsCheck = true;
- EnsureWindowOnScreen("on-load");
+ // Manual reset snaps unconditionally; on-load check only fires when the
+ // stored position has no overlap with any visible viewport.
+ if (RequestPositionReset)
+ {
+ RequestPositionReset = false;
+ DidOnLoadBoundsCheck = true;
+ ApplySafeDefaultPosition("manual-reset");
+ }
+ else if (!DidOnLoadBoundsCheck)
+ {
+ DidOnLoadBoundsCheck = true;
+ EnsureWindowOnScreen("on-load");
+ }
}
if (resized)
@@ -700,12 +727,17 @@ public sealed class ChatLogWindow : Window
LastViewport = ImGui.GetWindowViewport().Handle;
WasDocked = ImGui.IsWindowDocked();
- if (IsChatMode && Plugin.InputPreview.IsDrawable)
+ // v1.4.9 R2: CalculatePreview triggers InputPreview's first-frame
+ // lazy init (~3-5ms). User-typing-driven, safe to defer one frame.
+ if (_firstFrameDone && IsChatMode && Plugin.InputPreview.IsDrawable)
Plugin.InputPreview.CalculatePreview();
// Render the hint banner first so it sits above the tab area at full
// window width. ImGui accounts for its height automatically.
- DrawV061HintBannerIfNeeded();
+ // v1.4.9 R2: skip on first frame (~3-5ms layout cost). The banner
+ // is a v0.6.1 migration notice that returns the same result frame 1.
+ if (_firstFrameDone)
+ DrawV061HintBannerIfNeeded();
if (Plugin.Config.SidebarTabView)
DrawTabSidebar();
@@ -938,7 +970,11 @@ public sealed class ChatLogWindow : Window
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
// damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen.
- Plugin.StatusBar.Draw(Plugin);
+ // v1.4.9 R2: skip on the first frame; ~12ms of first-frame layout
+ // cost. User sees the StatusBar 1 frame (~17ms at 60fps) later
+ // which is hidden inside the post-reload Atlas-Build window.
+ if (_firstFrameDone)
+ Plugin.StatusBar.Draw(Plugin);
}
internal Dictionary GetValidChannels()
@@ -989,6 +1025,16 @@ public sealed class ChatLogWindow : Window
private void DrawChannelName(Tab activeTab)
{
+ // v1.4.9 R2: plain-text fallback on the first frame. ReadChannelName
+ // builds SeString chunks and DrawChunks runs SeString-Renderer layout
+ // — together ~18ms first-frame. Frame 1 renders the real chunks; the
+ // user sees the tab name for ~17ms during the post-reload window.
+ if (!_firstFrameDone)
+ {
+ ImGui.TextUnformatted(activeTab.Name);
+ return;
+ }
+
var currentChannel = ReadChannelName(activeTab);
if (!currentChannel.SequenceEqual(PreviousChannel))
PreviousChannel = currentChannel;
From d9f6704316e36dd85a2853f2e534eb4a81bc03dd Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Fri, 15 May 2026 11:31:35 +0200
Subject: [PATCH 4/8] chore: bump version to 1.4.9, sync manifest
Manifest version bump for the v1.4.9 release cut. Schema-required v16
stays unchanged (R1/R2/R3 are all config-neutral refactors).
Files touched:
- HellionChat/HellionChat.csproj: 1.4.8 -> 1.4.9
- HellionChat/Plugin.cs: schema-migration error string self-reference
(v1.4.8 -> v1.4.9, required schema v16 stays)
- repo.json: AssemblyVersion, TestingAssemblyVersion, 3x DownloadLink*
URLs all bumped to 1.4.9 / v1.4.9. Changelog field is still on v1.4.8;
the v1.4.9 block plus v1.4.5 slim-drop land in the next commit.
- README.md: shield badge, version header in lead paragraph, project-
status block rewritten for v1.4.9 (Plugin-Load Render Polish).
- docs/CHANGELOG.md: v1.4.9 block inserted above v1.4.8.
- docs/ROADMAP.md: v1.4.9 moved into the released-versions list,
"Next Cycle" header now targets v1.4.10 (Render Clipper + Symbol
Picker reserves carried over from the v1.4.9 plan).
yaml changelog block and repo.json Changelog field follow in the
docs commit so the slim-drop of v1.4.5 stays atomic with the v1.4.9
block insert.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
HellionChat/HellionChat.csproj | 2 +-
HellionChat/Plugin.cs | 4 ++--
README.md | 29 ++++++++++++++---------------
docs/CHANGELOG.md | 29 +++++++++++++++++++++++++++++
docs/ROADMAP.md | 29 +++++++++++++++++++++++++----
repo.json | 10 +++++-----
6 files changed, 76 insertions(+), 27 deletions(-)
diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj
index 14a864c..6c0ea5b 100644
--- a/HellionChat/HellionChat.csproj
+++ b/HellionChat/HellionChat.csproj
@@ -1,7 +1,7 @@
- 1.4.8
+ 1.4.9enableenable
diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs
index 8049444..6c9301e 100755
--- a/HellionChat/Plugin.cs
+++ b/HellionChat/Plugin.cs
@@ -189,8 +189,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Config.Version < 16)
{
throw new InvalidOperationException(
- $"HellionChat v1.4.8 requires config schema v16, got v{Config.Version}. "
- + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.8."
+ $"HellionChat v1.4.9 requires config schema v16, got v{Config.Version}. "
+ + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.9."
);
}
Config.Version = 17;
diff --git a/README.md b/README.md
index a16fc3e..a2771a1 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
[](LICENSE)
-[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
+[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
[](https://github.com/goatcorp/Dalamud)
[](https://dotnet.microsoft.com/)
[](https://www.finalfantasyxiv.com/)
@@ -11,7 +11,7 @@
-**Version 1.4.8** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
+**Version 1.4.9** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
@@ -286,19 +286,18 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
## Project Status
-**Version 1.4.8** — Hook-Layer and Polish Quick-Wins. The Database Viewer now has an optional FTS5 full-text search
-across the entire chat history. Toggle "Full-text search" next to the search bar; the index is built asynchronously on
-first run after the update with a progress toast, and the toggle stays disabled until the build completes. Multi-word
-terms match as exact phrases by default; power users can opt into raw FTS5 `MATCH` syntax by wrapping their own
-double-quotes. Custom theme files auto-reload when edited while the theme is active — save the JSON in your editor and
-the live render picks up the change within a second, no picker click. Retention sweep no longer blocks the framework
-thread (`Framework.Run(...).Wait()` replaced by `Framework.RunOnTick(...)`), removing the ~194 ms hitch per sweep. Status
-bar height is now derived from `GetTextLineHeightWithSpacing()` plus a DPI-aware spacer so the bar renders correctly at
-Windows display scaling above 100 %. Receive-suppressed-tells routing is postponed to v1.5.x; the investigation in this
-cycle showed that the FFXIV `ContentIdResolverHook` does not fire when other plugins suppress tells via
-`CheckMessageHandled`, which means tell-partner identification breaks for AutoTellTab routing — the fix lives next to
-the planned ad-block hook layer where the same `RaptureLogModule` patch surface comes up anyway. Migration v17 stays
-(no schema bump). Ninth sub-patch of the v1.4.x polish sweep series (as of 2026-05-14).
+**Version 1.4.9** — Plugin-Load Render Polish. First-frame render cost is now well under Dalamud's 100 ms HITCH
+warning threshold (~76 ms median, down from ~127 ms). The gain comes from deferring six non-essential rendering
+sections on the very first Draw — bottom status bar, channel-name SeString chunks, window bounds check, hint
+banner, autocomplete and input-preview calculation — so the initial ImGui layout cost is spread between frame 0
+and frame 1 instead of all hitting at once. At 60 fps the user sees those sections one frame (~17 ms) later, which
+is invisible inside the post-reload font-atlas build window. Slash commands `/hellion`, `/hellionView`,
+`/hellionSeString` and `/hellionDebugger` are now registered centrally during plugin load so they work before
+their target window is opened the first time. The configuration-button entry in Dalamud's plugin manager hangs on
+the same path. Three plugin-load profiling logs (auto-translate warm-up, message-store connect, tab filter) stay
+on at Information level as a regression tripwire — if a future change pushes the load past 100 ms again, the cost
+is right there in `/xllog`. Migration v17 stays (no schema bump). Tenth sub-patch of the v1.4.x polish sweep
+series (as of 2026-05-15).
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 0b35387..87c15ff 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -10,6 +10,35 @@ to the release pages for details.
---
+## Hellion Chat 1.4.9 — Plugin-Load Render Polish (2026-05-15)
+
+Tenth sub-patch of the v1.4.x polish-sweep series. First-frame render cost drops from ~127 ms median down to
+~76 ms median — comfortably under Dalamud's 100 ms HITCH warning threshold. The remaining ~13 ms gap to ChatTwo
+upstream (~63 ms median) is the cost of HellionChat-only features (sidebar tab view, custom status bar,
+Honorific integration).
+
+- First-frame defer: six non-essential rendering sections inside `ChatLogWindow` skip their first Draw and run
+ one frame later. Covered sections are the bottom status bar, channel-name SeString chunks, window bounds
+ check, v0.6.1 hint banner, autocomplete and input-preview calculation. At 60 fps the user sees those sections
+ ~17 ms after plugin reload — invisible inside the ~2.5 s font-atlas build window every reload runs through
+ anyway. Frame 1 stays well under 100 ms too (~40 ms), so no secondary HITCH warning appears.
+- Slash-command centralisation: `/hellion`, `/hellionView`, `/hellionSeString` and `/hellionDebugger` are now
+ registered during `LoadAsync` instead of inside the corresponding window constructors. The commands work
+ before their target window is opened the first time, and Dalamud's plugin-manager configuration / open
+ buttons (`UiBuilder.OpenConfigUi` / `OpenMainUi`) hang on the same path.
+- Plugin-load profiling logs stay on: `MessageStore.Connect`, `MessageStore.Migrate`, `FilterAllTabs` and the
+ auto-translate warm-up timing log are now Information level rather than Debug. They serve as a tripwire so a
+ future regression past 100 ms shows up directly in `/xllog` without re-enabling Debug.
+- Migration v17 stays (no schema bump).
+- Internal: hypothesis-triage during the R2 cycle falsified three of the four candidate root causes
+ (font-atlas sync, theme-apply ABGR-cache init, multiple-window render). Actual cause is `DrawList` setup
+ cost distributed across ~10 ImGui sections inside ChatLogWindow (5-20 ms each). The six selective defers
+ above are the pragmatic fix — a clean structural rewrite would belong in the v1.5.x DI-container cycle.
+
+Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
+
+---
+
## Hellion Chat 1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)
Ninth sub-patch of the v1.4.x polish-sweep series. Hook-layer cluster (FTS5 full-text search, ad-block foundation
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 539b51f..4968b4d 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -10,14 +10,35 @@ the plugin's privacy-first scope during brainstorming.
---
-## Next Cycle (v1.4.9)
+## Next Cycle (v1.4.10)
-**Plugin-Load Render Polish.** Erststart-Frame-Hitch (~110 ms UiBuilder) and the related Font-Atlas + Auto-Translate
-warmup costs surface every load and are reproducible in `/xlstats`. The cycle also unblocks the lazy-window refactor
-sketched in `feedback_lazy_window_dalamud` and the slash-command centralisation that comes with it.
+**Render Clipper, Symbol-Picker and Final-Cleanup.** Reserve items inherited from the v1.4.9 plan that did not need to
+land in the HITCH-cut: an `ImGuiListClipper` for variable-height messages in `DrawMessages` (the OtterGui `ImGuiClip.cs`
+wrapper is the idiom anchor), a Symbol Picker popup for the chat input (`imgui_demo.cpp` Popups & Modal Windows section
+is the pattern reference), plus the carry-over from v1.4.9: structural First-Frame-Layout rewrite if the v1.4.9 selective
+defers turn out to be too narrow once user-side regressions surface. Lazy-Window-Init naive is **not** in scope — the
+v1.4.9 Stage-2 diagnose falsified that path (`WindowSystem.windows` is non-thread-safe, Game-Freeze under reload stress,
+no measurable HITCH delta). A clean DI-container adoption (Lightless `PluginHostFactory` pattern) belongs in v1.5.x and
+will revisit the question with the right threading model.
---
+## v1.4.9 — Plugin-Load Render Polish (released 2026-05-15)
+
+Tenth sub-patch of the v1.4.x Polish Sweep series. First-frame HITCH drops from ~127 ms median to ~76 ms median (4-reload
+sample), comfortably under Dalamud's 100 ms warning threshold. Mechanism: a single `_firstFrameDone` flag inside
+`ChatLogWindow` defers six non-essential rendering sections (bottom status bar, channel-name SeString chunks, window
+bounds check, v0.6.1 hint banner, autocomplete, input-preview calculation) from frame 0 to frame 1. User sees those
+sections ~17 ms (60 fps) later, invisible inside the ~2.5 s font-atlas build window after every reload. Slash-command
+registration moved from individual window constructors to a central `SetupCommands` / `TearDownCommands` pair in
+`Plugin.cs` — `/hellion`, `/hellionView`, `/hellionSeString` and `/hellionDebugger` work before their target windows are
+opened the first time, and Dalamud's plugin-manager `OpenConfigUi` / `OpenMainUi` buttons hang on the same path.
+Plugin-load profiling logs (auto-translate warmup, `MessageStore.Connect`, `MessageStore.Migrate`, `FilterAllTabs`) stay
+on at Information level as a regression tripwire. Migration v17 stays (no schema bump). Hypothesis-triage falsified
+three of four candidate root causes (font-atlas sync fallback, theme-apply ABGR-cache init, multiple-window render via
+lazy-init) — actual cost distributes evenly across ~10 ImGui sections inside ChatLogWindow, so structural rewrite is
+deferred to v1.5.x DI-container cycle.
+
## v1.4.8 — Hook-Layer and Polish Quick-Wins (released 2026-05-14)
Ninth sub-patch of the v1.4.x Polish Sweep series. Database Viewer gains an optional FTS5 full-text search across the
diff --git a/repo.json b/repo.json
index d5c439f..c25b6fe 100644
--- a/repo.json
+++ b/repo.json
@@ -3,7 +3,7 @@
"Author": "Jon Kazama (Hellion Forge)",
"Name": "Hellion Chat",
"InternalName": "HellionChat",
- "AssemblyVersion": "1.4.8.0",
+ "AssemblyVersion": "1.4.9.0",
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
"ApplicableVersion": "any",
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
@@ -16,10 +16,10 @@
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
"Changelog": "**v1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)**\n\nNinth sub-patch of the v1.4.x polish-sweep series. Hook-layer cluster (DbViewer FTS5 full-text search, ad-block foundation investigation) plus three polish quick-wins.\n\n- DbViewer full-text search: optional FTS5 index across the full chat history. Built asynchronously on first load after the update with a progress toast. The local page-filter remains available as the default mode. Queries match as exact phrases -- multi-word terms must appear together in order; advanced users can opt into raw FTS5 MATCH syntax by wrapping their own double-quotes.\n- Custom theme files now auto-reload when edited while the theme is active -- no need to re-click the theme in the picker.\n- Retention sweep no longer blocks the framework thread, removing the ~194ms mini-hitch per sweep.\n- Status bar renders correctly at Windows display scaling > 100%.\n- Receive-suppressed-tells routing investigated this cycle and postponed to v1.5.x: when other plugins suppress tells via CheckMessageHandled, the FFXIV chat pipeline skips the RaptureLogModule.AddMsgSourceEntry path so HellionChat's ContentIdResolverHook does not fire and tell-partner identification breaks. The fix belongs next to the planned ad-block hook layer where the same patch surface comes up.\n- Internal: messages.Id is declared BLOB but stored as TEXT (Microsoft.Data.Sqlite Guid binding). FTS bulk insert and LoadByGuids match the TEXT storage form on both sides. Migration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**\n\nEighth sub-patch of the v1.4.x polish-sweep series. First user-visible feature bundle since v1.4.5 — pinned tell tabs that survive relog, opt-in Honorific glow rendering, and a configurable sidebar.\n\n- TempTell Pin: right-click a TempTell tab in the sidebar to pin it. Pinned tabs survive relog, keep their conversation history (loaded on demand from the message store), and stay bound to the same /tell partner. Hard cap of 5 pinned tabs in a pool separate from the 15-tab auto-tell pool — total ceiling is 20 tabs. New 'Angepinnt' / 'Pinned' section in the sidebar with its own divider header\n- Honorific Glow outline now renders when the title carries a Glow colour. Opt-in via Settings → Integrations → 'Render glow outlines (Honorific)' (default off, dodges the per-frame DrawList overhead on low-end hardware). Gradient (Color3 / GradientColourSet / Wave / Pulse) is parsed but rendered statically — a later cycle will port the full animation\n- Sidebar width is now configurable in Theme & Layout (range 44–160 px). Default stays icon-only; widen to fit section headers like 'Aktive Tells (3)' without truncation\n- Settings Save no longer pops the chat input back to /tell with a pinned partner — Configuration.UpdateFrom now preserves the runtime CurrentChannel across the persistent-tab merge, and TabSwitched deep-clones the seeded channel instead of sharing the previous tab's UsedChannel\n- Util/ImGuiUtil.cs DrawArrows IconButton id now uses (id + 1).ToString() instead of the operator-precedence quirk id + 1.ToString() — generated IDs stay numerically stable\n- Internal: IPluginLogProxy indirection over Dalamud's IPluginLog routes all ~91 Plugin.Log call sites through a testable proxy. MessageStore.Migrate0 can now run in xUnit without loading Dalamud.dll, closing the gap F12.1 left in v1.4.6\n- Internal: TempTab counter switched from an Interlocked cached field to a derived Tabs.Count(predicate) — pin-state transitions are cold-path and don't need lock-free reads\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n- Built-in themes: Crystal Nocturne (sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom. Users with Moonlit Bloom selected fall back to Hellion Arctic on first load\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
"AcceptsFeedback": true,
- "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.8/latest.zip",
- "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.8/latest.zip",
- "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.8/latest.zip",
- "TestingAssemblyVersion": "1.4.8.0",
+ "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.9/latest.zip",
+ "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.9/latest.zip",
+ "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.9/latest.zip",
+ "TestingAssemblyVersion": "1.4.9.0",
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
"ImageUrls": [
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
From c6a37807532739d217df7f1f765a951e5be145e5 Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Fri, 15 May 2026 11:51:25 +0200
Subject: [PATCH 5/8] docs: add v1.4.9 changelog and forge announcement post
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Synchronises the v1.4.9 changelog across the manifest sources that the
Dalamud plugin installer, the gitea repo.json feed and the Forge auto-
announce workflow read at release-tag time.
Files touched:
- HellionChat/HellionChat.yaml: v1.4.9 block inserted at the top of the
changelog: literal. v1.4.5 dropped to keep the slim-rule at 4 subblocks
(preflight Block C enforces YAML_VERSIONS <= 4). Current set is
v1.4.9/v1.4.8/v1.4.7/v1.4.6.
- repo.json: Changelog field kept synchronous with the yaml — v1.4.9
block prepended, v1.4.5 substring removed, JSON-escaped newlines.
- .github/forge-posts/v1.4.9.md: new file with frontmatter (subtitle
"Plugin-Load Render Polish", versionsnatur "Performance-Patch") and
a German-only body. The English half of the eventual Discord embed
is pulled automatically from the yaml changelog at tag-push time by
.gitea/workflows/forge-announce.yml — same workflow as v1.4.4
onwards, the post file does not carry an English block.
Char-cap pre-check passes (title 46 + description ~2700 + footer 33 =
~2800 chars, well under the 5500-char Discord embed total cap).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.github/forge-posts/v1.4.9.md | 28 ++++++++++++++++++++
HellionChat/HellionChat.yaml | 50 +++++++++++++++++++----------------
repo.json | 2 +-
3 files changed, 56 insertions(+), 24 deletions(-)
create mode 100644 .github/forge-posts/v1.4.9.md
diff --git a/.github/forge-posts/v1.4.9.md b/.github/forge-posts/v1.4.9.md
new file mode 100644
index 0000000..41928d0
--- /dev/null
+++ b/.github/forge-posts/v1.4.9.md
@@ -0,0 +1,28 @@
+---
+subtitle: Plugin-Load Render Polish
+versionsnatur: Performance-Patch
+---
+
+- First-Frame-HITCH unter 100 ms: der erste Render-Frame des Plugins liegt
+ jetzt bei ~76 ms Median (vorher ~127 ms), die Dalamud-Warnung
+ „UiBuilder(Hellion Chat) > 100ms" beim Plugin-Start ist damit weg.
+ Erreicht durch das Verlagern von sechs nicht-essentiellen Render-
+ Sektionen (Statusleiste, Kanalname-Chunks, Fenster-Bounds-Check,
+ Hinweis-Banner, Autocomplete, Input-Preview) auf den zweiten Frame.
+ Bei 60 fps sieht man die deferred-Sektionen ~17 ms später, was im
+ Atlas-Build-Fenster nach einem Reload unsichtbar bleibt.
+- Slash-Commands zentral registriert: /hellion, /hellionView,
+ /hellionSeString und /hellionDebugger werden jetzt im Plugin-Load zentral
+ registriert statt erst beim ersten Öffnen ihres Ziel-Fensters. Heißt: die
+ Befehle funktionieren ab dem ersten Tick, auch wenn das jeweilige Fenster
+ nie geöffnet wurde. Der „Einstellungen"-Button im Plugin-Manager hängt am
+ selben Pfad.
+- Plugin-Load-Diagnose-Logs als Tripwire: die Profiling-Logs für
+ MessageStore.Connect, MessageStore.Migrate, FilterAllTabs und den
+ Auto-Translate-Warmup bleiben auf Information-Level eingeschaltet. Falls
+ eine zukünftige Änderung die Lade-Zeit wieder über 100 ms drückt, taucht
+ der Mehrverbrauch direkt im /xllog auf, ohne dass jemand erst den
+ Debug-Filter einschalten muss.
+- Migration v17 unverändert: kein Schema-Bump, kein Config-Migrations-
+ Aufwand. Nach dem Update läuft das Plugin gegen die bestehende
+ v17-Datenbank weiter.
diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml
index a81bbd2..01b78f8 100755
--- a/HellionChat/HellionChat.yaml
+++ b/HellionChat/HellionChat.yaml
@@ -35,6 +35,33 @@ tags:
- Replacement
- Privacy
changelog: |-
+ **v1.4.9 — Plugin-Load Render Polish (2026-05-15)**
+
+ Tenth sub-patch of the v1.4.x polish-sweep series. First-frame
+ render cost drops from ~127 ms median to ~76 ms median,
+ comfortably under Dalamud's 100 ms HITCH warning threshold.
+
+ - First-frame defer: six non-essential rendering sections inside
+ ChatLogWindow skip their first Draw and run one frame later
+ (bottom status bar, channel-name SeString chunks, window bounds
+ check, v0.6.1 hint banner, autocomplete, input-preview
+ calculation). User-visible delay is ~17 ms at 60 fps, hidden
+ inside the post-reload font-atlas build window.
+ - Slash-command centralisation: /hellion, /hellionView,
+ /hellionSeString and /hellionDebugger are registered in
+ LoadAsync instead of inside the corresponding window
+ constructors. The plugin-manager Open and configuration buttons
+ hang on the same path.
+ - Plugin-load profiling logs stay on at Information level
+ (MessageStore connect/migrate, FilterAllTabs, auto-translate
+ warmup) as a regression tripwire — a future load past 100 ms
+ will show up in /xllog without a Debug filter.
+ - Migration v17 stays (no schema bump).
+
+ Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
+
+ ---
+
**v1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)**
Ninth sub-patch of the v1.4.x polish-sweep series. Hook-layer
@@ -151,27 +178,4 @@ changelog: |-
---
- **v1.4.5 — UX and Robustness (2026-05-12)**
-
- Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
- failures surface as a notification, the first-run wizard has an
- explicit "Later" option, the input history clears on plugin reload,
- and the status bar version slot stops clipping in narrow windows.
-
- - Chat window draw errors now show a one-shot notification instead
- of failing silently — stack trace stays in /xllog
- - First-run wizard: explicit "Later — keep defaults" button.
- Closing the X no longer silently accepts the defaults; the wizard
- reopens on the next plugin load if nothing was picked
- - InputHistoryService clears on plugin dispose so the previous
- session's typed commands don't bleed into the next load
- - Status bar hides the version slot when the chat window is too
- narrow to fit all five slots without overlap
- - Internal: explicit session-only Auto-Tell-Tab invariant in
- Plugin.cs plus a pinning test in the Build-Suite
- - Internal: FontManager falls back to the system font if the
- embedded Hellion font resource is missing — logs a Warning
-
- ---
-
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
diff --git a/repo.json b/repo.json
index c25b6fe..0ddc1ae 100644
--- a/repo.json
+++ b/repo.json
@@ -14,7 +14,7 @@
"CanUnloadAsync": false,
"LoadPriority": 0,
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
- "Changelog": "**v1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)**\n\nNinth sub-patch of the v1.4.x polish-sweep series. Hook-layer cluster (DbViewer FTS5 full-text search, ad-block foundation investigation) plus three polish quick-wins.\n\n- DbViewer full-text search: optional FTS5 index across the full chat history. Built asynchronously on first load after the update with a progress toast. The local page-filter remains available as the default mode. Queries match as exact phrases -- multi-word terms must appear together in order; advanced users can opt into raw FTS5 MATCH syntax by wrapping their own double-quotes.\n- Custom theme files now auto-reload when edited while the theme is active -- no need to re-click the theme in the picker.\n- Retention sweep no longer blocks the framework thread, removing the ~194ms mini-hitch per sweep.\n- Status bar renders correctly at Windows display scaling > 100%.\n- Receive-suppressed-tells routing investigated this cycle and postponed to v1.5.x: when other plugins suppress tells via CheckMessageHandled, the FFXIV chat pipeline skips the RaptureLogModule.AddMsgSourceEntry path so HellionChat's ContentIdResolverHook does not fire and tell-partner identification breaks. The fix belongs next to the planned ad-block hook layer where the same patch surface comes up.\n- Internal: messages.Id is declared BLOB but stored as TEXT (Microsoft.Data.Sqlite Guid binding). FTS bulk insert and LoadByGuids match the TEXT storage form on both sides. Migration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**\n\nEighth sub-patch of the v1.4.x polish-sweep series. First user-visible feature bundle since v1.4.5 — pinned tell tabs that survive relog, opt-in Honorific glow rendering, and a configurable sidebar.\n\n- TempTell Pin: right-click a TempTell tab in the sidebar to pin it. Pinned tabs survive relog, keep their conversation history (loaded on demand from the message store), and stay bound to the same /tell partner. Hard cap of 5 pinned tabs in a pool separate from the 15-tab auto-tell pool — total ceiling is 20 tabs. New 'Angepinnt' / 'Pinned' section in the sidebar with its own divider header\n- Honorific Glow outline now renders when the title carries a Glow colour. Opt-in via Settings → Integrations → 'Render glow outlines (Honorific)' (default off, dodges the per-frame DrawList overhead on low-end hardware). Gradient (Color3 / GradientColourSet / Wave / Pulse) is parsed but rendered statically — a later cycle will port the full animation\n- Sidebar width is now configurable in Theme & Layout (range 44–160 px). Default stays icon-only; widen to fit section headers like 'Aktive Tells (3)' without truncation\n- Settings Save no longer pops the chat input back to /tell with a pinned partner — Configuration.UpdateFrom now preserves the runtime CurrentChannel across the persistent-tab merge, and TabSwitched deep-clones the seeded channel instead of sharing the previous tab's UsedChannel\n- Util/ImGuiUtil.cs DrawArrows IconButton id now uses (id + 1).ToString() instead of the operator-precedence quirk id + 1.ToString() — generated IDs stay numerically stable\n- Internal: IPluginLogProxy indirection over Dalamud's IPluginLog routes all ~91 Plugin.Log call sites through a testable proxy. MessageStore.Migrate0 can now run in xUnit without loading Dalamud.dll, closing the gap F12.1 left in v1.4.6\n- Internal: TempTab counter switched from an Interlocked cached field to a derived Tabs.Count(predicate) — pin-state transitions are cold-path and don't need lock-free reads\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n- Built-in themes: Crystal Nocturne (sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom. Users with Moonlit Bloom selected fall back to Hellion Arctic on first load\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
+ "Changelog": "**v1.4.9 — Plugin-Load Render Polish (2026-05-15)**\n\nTenth sub-patch of the v1.4.x polish-sweep series. First-frame render cost drops from ~127 ms median to ~76 ms median, comfortably under Dalamud's 100 ms HITCH warning threshold.\n\n- First-frame defer: six non-essential rendering sections inside ChatLogWindow skip their first Draw and run one frame later (bottom status bar, channel-name SeString chunks, window bounds check, v0.6.1 hint banner, autocomplete, input-preview calculation). User-visible delay is ~17 ms at 60 fps, hidden inside the post-reload font-atlas build window.\n- Slash-command centralisation: /hellion, /hellionView, /hellionSeString and /hellionDebugger are registered in LoadAsync instead of inside the corresponding window constructors. The plugin-manager Open and configuration buttons hang on the same path.\n- Plugin-load profiling logs stay on at Information level (MessageStore connect/migrate, FilterAllTabs, auto-translate warmup) as a regression tripwire — a future load past 100 ms will show up in /xllog without a Debug filter.\n- Migration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)**\n\nNinth sub-patch of the v1.4.x polish-sweep series. Hook-layer cluster (DbViewer FTS5 full-text search, ad-block foundation investigation) plus three polish quick-wins.\n\n- DbViewer full-text search: optional FTS5 index across the full chat history. Built asynchronously on first load after the update with a progress toast. The local page-filter remains available as the default mode. Queries match as exact phrases -- multi-word terms must appear together in order; advanced users can opt into raw FTS5 MATCH syntax by wrapping their own double-quotes.\n- Custom theme files now auto-reload when edited while the theme is active -- no need to re-click the theme in the picker.\n- Retention sweep no longer blocks the framework thread, removing the ~194ms mini-hitch per sweep.\n- Status bar renders correctly at Windows display scaling > 100%.\n- Receive-suppressed-tells routing investigated this cycle and postponed to v1.5.x: when other plugins suppress tells via CheckMessageHandled, the FFXIV chat pipeline skips the RaptureLogModule.AddMsgSourceEntry path so HellionChat's ContentIdResolverHook does not fire and tell-partner identification breaks. The fix belongs next to the planned ad-block hook layer where the same patch surface comes up.\n- Internal: messages.Id is declared BLOB but stored as TEXT (Microsoft.Data.Sqlite Guid binding). FTS bulk insert and LoadByGuids match the TEXT storage form on both sides. Migration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**\n\nEighth sub-patch of the v1.4.x polish-sweep series. First user-visible feature bundle since v1.4.5 — pinned tell tabs that survive relog, opt-in Honorific glow rendering, and a configurable sidebar.\n\n- TempTell Pin: right-click a TempTell tab in the sidebar to pin it. Pinned tabs survive relog, keep their conversation history (loaded on demand from the message store), and stay bound to the same /tell partner. Hard cap of 5 pinned tabs in a pool separate from the 15-tab auto-tell pool — total ceiling is 20 tabs. New 'Angepinnt' / 'Pinned' section in the sidebar with its own divider header\n- Honorific Glow outline now renders when the title carries a Glow colour. Opt-in via Settings → Integrations → 'Render glow outlines (Honorific)' (default off, dodges the per-frame DrawList overhead on low-end hardware). Gradient (Color3 / GradientColourSet / Wave / Pulse) is parsed but rendered statically — a later cycle will port the full animation\n- Sidebar width is now configurable in Theme & Layout (range 44–160 px). Default stays icon-only; widen to fit section headers like 'Aktive Tells (3)' without truncation\n- Settings Save no longer pops the chat input back to /tell with a pinned partner — Configuration.UpdateFrom now preserves the runtime CurrentChannel across the persistent-tab merge, and TabSwitched deep-clones the seeded channel instead of sharing the previous tab's UsedChannel\n- Util/ImGuiUtil.cs DrawArrows IconButton id now uses (id + 1).ToString() instead of the operator-precedence quirk id + 1.ToString() — generated IDs stay numerically stable\n- Internal: IPluginLogProxy indirection over Dalamud's IPluginLog routes all ~91 Plugin.Log call sites through a testable proxy. MessageStore.Migrate0 can now run in xUnit without loading Dalamud.dll, closing the gap F12.1 left in v1.4.6\n- Internal: TempTab counter switched from an Interlocked cached field to a derived Tabs.Count(predicate) — pin-state transitions are cold-path and don't need lock-free reads\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n- Built-in themes: Crystal Nocturne (sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom. Users with Moonlit Bloom selected fall back to Hellion Arctic on first load\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
"AcceptsFeedback": true,
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.9/latest.zip",
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.9/latest.zip",
From 8c4afaac1723ea4dd785f0bb920de5218b2165d2 Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Fri, 15 May 2026 12:47:06 +0200
Subject: [PATCH 6/8] feat(ipc): mirror TypingIpc provider slots under ChatTwo
namespace (v1.4.9 R4)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
HellionChat replaces ChatTwo (conflict detection prevents parallel loading)
but third-party plugins with a no-fork policy keep subscribing only to the
ChatTwo.*-prefixed IPC gates. Mirroring the two TypingIpc provider slots
under the ChatTwo namespace lets those plugins keep working without code
changes on their side.
Mirrored slots:
- ChatTwo.GetChatInputState ←→ HellionChat.GetChatInputState
- ChatTwo.ChatInputStateChanged ←→ HellionChat.ChatInputStateChanged
Implementation:
- Two additional ICallGateProvider fields (ChatTwoStateQueryGate +
ChatTwoStateChangedGate) with the identical ChatInputState tuple
signature. The tuple's underlying types match ChatTwo's surface byte-
for-byte (bool/bool/bool/bool/int/ushort — ChatType is `ushort` in both
repos), so Dalamud's IPC marshalling matches across plugin boundaries
even when the subscribing plugin defines its own copy of the ChatType
enum.
- ctor registers the new provider gates and binds RegisterFunc(GetState)
to ChatTwoStateQueryGate so query calls route to the same backing path.
- Update() pushes the state to both ChatTwoStateChangedGate and the
existing StateChangedGate in lockstep.
- Dispose() unregisters both query gates.
Ipc/ExtraChat.cs is intentionally unchanged — it is a subscriber on
ExtraChat's own IPC, not a provider, so no compatibility mirror applies.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
HellionChat/Ipc/TypingIpc.cs | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/HellionChat/Ipc/TypingIpc.cs b/HellionChat/Ipc/TypingIpc.cs
index 474c8ff..33b6fd6 100644
--- a/HellionChat/Ipc/TypingIpc.cs
+++ b/HellionChat/Ipc/TypingIpc.cs
@@ -19,6 +19,17 @@ internal sealed class TypingIpc : IDisposable
private ICallGateProvider StateQueryGate { get; }
private ICallGateProvider StateChangedGate { get; }
+ // v1.4.9 R4: ChatTwo IPC compatibility mirror. Some third-party plugins
+ // have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC
+ // gates. HellionChat replaces ChatTwo (conflict detection prevents
+ // parallel loading), so mirroring the ChatTwo provider slots lets those
+ // plugins keep working without code changes on their side. The tuple
+ // shape is textually identical to ChatTwo's IPC surface (same member
+ // order, same underlying types — ChatType is `ushort` in both repos)
+ // so Dalamud's IPC marshalling matches across plugin boundaries.
+ private ICallGateProvider ChatTwoStateQueryGate { get; }
+ private ICallGateProvider ChatTwoStateChangedGate { get; }
+
private ChatInputState LastState;
private bool HasState;
@@ -33,7 +44,16 @@ internal sealed class TypingIpc : IDisposable
"HellionChat.ChatInputStateChanged"
);
+ // v1.4.9 R4: ChatTwo-prefixed compatibility slots (see class-level comment).
+ ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider(
+ "ChatTwo.GetChatInputState"
+ );
+ ChatTwoStateChangedGate = Plugin.Interface.GetIpcProvider(
+ "ChatTwo.ChatInputStateChanged"
+ );
+
StateQueryGate.RegisterFunc(GetState);
+ ChatTwoStateQueryGate.RegisterFunc(GetState);
}
private ChatInputState BuildState()
@@ -67,10 +87,13 @@ internal sealed class TypingIpc : IDisposable
HasState = true;
LastState = state;
StateChangedGate.SendMessage(state);
+ // v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
+ ChatTwoStateChangedGate.SendMessage(state);
}
public void Dispose()
{
StateQueryGate.UnregisterFunc();
+ ChatTwoStateQueryGate.UnregisterFunc();
}
}
From 655c903cb502e55d26c00a43c7aaa5fa791887a0 Mon Sep 17 00:00:00 2001
From: Jon Kazama
Date: Fri, 15 May 2026 13:01:12 +0200
Subject: [PATCH 7/8] feat(ipc): mirror context-menu IPC gates under ChatTwo
namespace (v1.4.9 R4 ext)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Extends commit 8c4afaa: the TypingIpc mirror covered only two of the six
ChatTwo IPC slots. Third-party plugins like Artisan and AllaganTools
subscribe to a different ChatTwo IPC surface — the context-menu
integration (ChatTwo.Register / Unregister / Available / Invoke) that
lets them push item-links into the chat. Smoke test against the
deployed v1.4.9 build showed Artisan logging "Chat2 is not available"
because those four gates were not yet mirrored.
This commit adds the missing four ChatTwo-prefixed provider gates in
IpcManager.cs:
- ChatTwo.Register (Func) — bound to the existing Register()
backing method, so plugins that subscribe via either namespace land
in the same Registered list.
- ChatTwo.Unregister (Action) — bound to the existing
Unregister() backing method, same shared-state rationale.
- ChatTwo.Available (Action<>) — SendMessage() fires from the ctor right
after AvailableGate.SendMessage(), so any subscriber waiting on the
"Chat 2 became available" signal sees both events.
- ChatTwo.Invoke (Action) — Invoke() fans the context-menu event out to
both InvokeGate and ChatTwoInvokeGate in lockstep. Subscribers compare
on the registration ID they got back from Register, so the
shared-backing approach keeps that contract intact regardless of which
namespace they subscribed under.
Dispose() unregisters all four ChatTwo gates plus the four existing
HellionChat gates. The conflict-detection that prevents ChatTwo from
loading alongside HellionChat guarantees no slot collision at runtime.
With this commit the full ChatTwo IPC surface (6 of 6 slots) is mirrored:
- ChatTwo.GetChatInputState (TypingIpc, commit 8c4afaa)
- ChatTwo.ChatInputStateChanged (TypingIpc, commit 8c4afaa)
- ChatTwo.Register (IpcManager, this commit)
- ChatTwo.Unregister (IpcManager, this commit)
- ChatTwo.Available (IpcManager, this commit)
- ChatTwo.Invoke (IpcManager, this commit)
Co-Authored-By: Claude Opus 4.7 (1M context)
---
HellionChat/IpcManager.cs | 49 +++++++++++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/HellionChat/IpcManager.cs b/HellionChat/IpcManager.cs
index b70e49f..cbade56 100755
--- a/HellionChat/IpcManager.cs
+++ b/HellionChat/IpcManager.cs
@@ -19,6 +19,26 @@ internal sealed class IpcManager : IDisposable
object?
> InvokeGate { get; }
+ // v1.4.9 R4: ChatTwo IPC compatibility mirror. Third-party plugins with
+ // a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the
+ // ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
+ // provider slots under the ChatTwo namespace lets those plugins keep
+ // working without code changes on their side. Conflict detection
+ // prevents ChatTwo and HellionChat from loading in parallel, so no slot
+ // collision risk.
+ private ICallGateProvider ChatTwoRegisterGate { get; }
+ private ICallGateProvider ChatTwoUnregisterGate { get; }
+ private ICallGateProvider