AgentMap.Instance() and AgentChatLog.Instance() can return null during
zone transitions. Capture pointers into locals and short-circuit the
FlagMarkerCount/LinkedItem deref when null so the entries are correctly
greyed out without faulting. Add Activate/ActivatePos after each append
so the input box regains focus and the caret lands after the token,
matching the SymbolPicker and AutoComplete insert paths.
Guard _childScrolledUp writes behind updateScrollState param so pop-out
windows no longer contaminate the main window's scroll state. Widen the
honorific title slot budget when the scroll button is visible, fix stale
comment, and apply csharpier formatting.
Drop the three-attempt floating overlay entirely. The button now lives in
the chat header toolbar (DrawScrollToBottomToolbarButton), visible only when
the user is scrolled above the live end. Toolbar layout: honorific slot,
scroll button, pop-out button flush-right -- pop-out position unchanged.
Button drawn in the parent window over the ##chat2-messages child was never
clickable: ImGui resolves g.HoveredWindow to the child for that screen rect, so
ItemHoverable rejects any item submitted in the parent. A top-level Begin/End
window is a sibling in the window list and wins the hit-test for its own rect.
ownerId parameter keeps the window name distinct between the main window and
each pop-out, preventing Begin/End collisions when both render in the same frame.
The button was drawn inside the ##chat2-messages child via SetCursorPos,
which inflated ContentSize.y / ScrollMaxY each frame (causing positional
drift) and was clipped by the scrollbar's inner clip rect (causing right-
edge cutoff). Move it to the parent window using screen-space coordinates
captured before the child opens; the scroll state is cached inside the
child while GetScrollMaxY/Y still refer to the child's scroll context.
Bumps csproj, yaml, repo.json, CHANGELOG, ROADMAP and README in
lock-step to 1.5.4. Forge-post DE-body added with the Polish & Motion
versionsnatur. Slim-rule applied to the yaml and repo.json changelog
blocks (keeps v1.5.4 + v1.5.3 + v1.5.2 + v1.5.1, drops v1.5.0).
A csharpier reflow of two v1.5.4 source files (ChatLogWindow,
HellionStyle) is folded in. preflight.sh blocks A-F all green.
Sidebar icons ease from 40% to 100% alpha on hover-in via FrameLerp
plus ApplyAlpha. Card-mode borders aggregate row-hover per tab and
lift the border alpha by up to ~+0x70 across every row in that tab.
borderColorAbgr moves into the loop so the per-iteration boost can
apply. ReduceMotion snaps both paths instantly.
Card-hover detection uses IsMouseHoveringRect over the row bounds --
IsItemHovered would only see the 2px spacer dummy below each row.
Palette button left of the cog opens a two-section popup. The themes
section enumerates AllBuiltIns + AllCustom; the tabs section
enumerates Config.Tabs. The active entry gets a leading check-glyph,
inactive rows a same-width blank so labels stay aligned. Click
selects without closing the popup (DontClosePopups).
Theme click triggers the PM-1 crossfade via ThemeRegistry.Switch;
tab click routes through ChangeTab so LastActivityTime stays
consistent with the sidebar and top-bar click paths.
The header input-width reservation now counts the new button plus
the per-button SameLine spacing -- the old formula dropped the
spacing term and overflowed the row once a third button appeared.
Slice D shrinks vs the original plan: three of the six files cannot
take an ILogger ctor arg without breaking external contracts.
Migrated (8 LogProxy sites across 4 files):
- Commands: 2 sites (Warning, Error). New ctor takes ILogger<Commands>.
- Themes/ThemeRegistry: 1 site (Debug). ILogger<ThemeRegistry>? is
optional (default null) so the existing Build-Suite tests that
construct `new ThemeRegistry()` parameterless keep working without
changes. _logger?.LogDebug guards the call site.
- PayloadHandler: 3 sites (Error, Warning, Error). New ctor takes
ILogger<PayloadHandler>. ChatLogWindow's two `new PayloadHandler(this)`
sites (the direct field and the Lender lambda) now hand a fresh
CreateLogger<PayloadHandler>() from the existing _loggerFactory.
Not migrated (5 sites stay on Plugin.LogProxy, plan drifts D12-D14):
- D12 - Configuration (1 site): IPluginConfiguration, instantiated by
Dalamud's Interface.GetPluginConfig() via reflection on the
parameterless ctor. Adding an ILogger arg would break GetPluginConfig.
- D13 - Message (4 sites): partial data class with two ctor overloads,
mass-instantiated across 3 plugin sites plus Newtonsoft JSON
deserialisation. Ctor extension would be invasive across ~20 call
sites with low payoff (data-class logger is unusual).
- D14 - FontManager (2 sites): both Plugin.LogProxy calls live in
static methods (TryGetHellionFontBytes, AddFontWithFallback) that
cannot reach an instance _logger. Same root cause as D8 in
GameFunctions. FontManager joins the static-bucket alongside
EmoteCache et al.; the ctor + _logger field added mid-Slice-D were
rolled back to keep the class clean.
Plugin.LogProxy surface after C9 (8 file buckets, ~12 sites total):
- 4 originally-static consumers: EmoteCache, AutoTranslate,
MemoryUtil, WrapperUtil
- 3 cannot-take-ctor-arg consumers: Configuration, Message, FontManager
- 1 single-static-method consumer: GameFunctions.TryOpenAdventurerPlate
(D8 from Slice B)
Smoke 2 is now due.
Six UI files shift from Plugin.LogProxy to ILogger<T> via
constructor injection.
Container singletons (each takes a typed ILogger plus, where it owns
nested allocations, an ILoggerFactory to spawn child loggers):
- Ui/ChatLogWindow (15 sites, plus an ILoggerFactory for the
Popout new-call at Ui/ChatLogWindow.cs:2417)
- Ui/Settings (SettingsWindow): no own sites, but takes an
ILoggerFactory so it can hand typed loggers to its three migrated
settings tabs (General, the other six tabs stay unchanged)
- Ui/DbViewer (3 sites)
Nested instances allocated by parent containers:
- Ui/Popout (7 sites, ILogger<Popout> as the new 4th ctor arg passed
from ChatLogWindow)
- Ui/SettingsTabs/ThemeAndLayout (1 site)
- Ui/SettingsTabs/FontsAndColours (1 site)
- Ui/SettingsTabs/DataManagement (15 sites)
PluginHostFactory factory lambdas updated for ChatLogWindow,
SettingsWindow and DbViewer to resolve the new logger args.
Cherry-pick from ChatTwo upstream ee7768ac (Infiziert90, 2026-05-16):
when args.AddIfNotPresent or args.Input starts with '/', replace the
chat input instead of appending. Fixes the Friend-List "/tell" path
where existing text like "test" would otherwise concatenate to
"test/tell user@world" before the receiver and channel resolve.
Variable drift versus upstream: HellionChat uses local 'Chat' where
ChatTwo uses InputHandler.ChatInput; logic is 1:1.
Forge-Post is required in the tagged tree so forge-announce.yml can read
it during the release-pipeline run. Plus a csharpier reflow on two files
(SymbolPicker.cs, ChatLogWindow.cs) that preflight Block E flagged after
the cycle's comment-tightening sweep — purely whitespace, no behaviour
change.
Adds a Configuration property, defaulted to enabled, and a checkbox in
the Chat settings tab's Behaviour section. Strings live in HellionStrings
so DE/EN stays in sync. Defaults aligned with our 'discoverable by
default, hidden by user choice' convention. Schema stays at v17 — the
new boolean is additive, the default constructor covers existing configs.
New popup attached to the chat input lets the user browse and insert
Dalamud SeIconChar glyphs (161 PUA codepoints, server-safe by design).
Search field filters by enum name. Multi-insert keeps the popup open
until the user clicks elsewhere. BMP tab follows in the next commit.
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) <noreply@anthropic.com>
Replace the fixed 22px const Height with a computed property that bakes
in the ImGui font line height plus a GlobalScale-rounded 2px spacer.
The constant clipped the bottom bar on Windows display-scaling >100%
because ImGui rendered the actual font taller than 22px; the bar then
got pushed off the window edge.
ChatLogWindow.cs:423 reservation drops the explicit +2 because the
spacer now lives inside Height. Same idiom as the v1.4.6 F7.2 underline
pill in ChatLogWindow.cs:1639-1653.
v1.4.8 B1. Coverage via in-game smoke on Windows (Jin) and Linux/Wayland
in Task 9 -- DrawList-coupled, no Build-Suite test.
Smoke-test round 4 surfaced a clean reproducer: a Party or Linkshell
tab with channel /p, then Settings → Save, popped the input back to
/tell <pinned-partner> on the next interaction. Two bugs combined:
1. Configuration.UpdateFrom captured only Messages+LastSendUnread from
the live state during the persistent-tab merge. CurrentChannel was
not preserved, so a Settings save overwrote the runtime channel
state with the settings-time snapshot. If the user switched channel
in-game between Settings-open and Settings-save, that switch was
lost. Live CurrentChannel now joins Messages and LastSendUnread in
the per-Identifier preservation tuple.
2. TabSwitched seeded a new tab's CurrentChannel from previousTab via
reference copy (`newTab.CurrentChannel = previousTab.CurrentChannel`).
That left both tabs sharing the same UsedChannel instance, so a
later mutation on one bled into the other — exactly the path that
carried a pinned tell-target onto Party. Switched to a deep clone
(UsedChannel.Clone(), same Cherry-Pick-Patch-B pattern from v1.4.6)
plus a Debug log so the next smoke can confirm at a glance which
previous tab donated its channel state.
Pre-existing ChatTwo upstream pattern; v1.4.7 just made it visible
because pinned tabs are now the kind of long-lived tell-target that
sticks around for the seed path to grab.
Smoke-test round 3 feedback from Jin:
- Sidebar now groups tabs into three sections rendered in this order:
persistent → pinned TempTabs → unpinned TempTabs. Each TempTab
section carries its own divider header ("Angepinnt (n)" / "Aktive
Tells (n)"). Plugin.Config.Tabs order is untouched — only the
display order changes, so tabI still mirrors the real index and
LastTab/WantedTab stay consistent.
- The thumbtack glyph overlay on a pinned tab dropped from accent
colour at full alpha to TextMuted at ~47% alpha. The section header
is now the primary discoverability cue; the glyph is just a per-tab
confirmation hint.
- Sidebar width is now a Config field (default 44, range 44-160).
Slider lives in Theme & Layout under the existing Sidebar-Tab-View
toggle. The icon button inside each row stretches with the width so
a widened sidebar doesn't leave the icon floating in dead space.
Smoke-test round 2 feedback from Jin:
- Promote-to-permanent label "Dauerhaft behalten" was indistinguishable
from Pin in German, leading to misclicks that dropped the tell-target.
Removed the menu entry from TempTabs entirely — Promote stays as a
service method for future use, but the user-facing path is gone. Anyone
who wants a regular tab can still create one via the existing
"neuen Tab anlegen" flow.
- No visual confirmation that pin took effect. Added a FontAwesome
thumbtack overlay top-left of the sidebar icon, accent-coloured, and
appended a "Pinned — survives relog" line to the hover tooltip.
- Pinned tabs came back empty after a full disable/enable cycle because
Tab.Messages is NonSerialized. RehydratePinnedTabs now also runs the
same MessageStore-backed PreloadHistory the spawn path uses, so the
recent conversation window reappears alongside the rehydrated
TellTarget.
Diagnose-logging on TryPin/Unpin/Promote/Rehydrate stays in so the next
smoke can confirm at a glance which path fired from the Dalamud console.
Honorific's TitleData carries Glow / Color3 / GradientColourSet /
GradientAnimationStyle beyond the Title + Color we parsed in Cycle 1.
The DTO now mirrors all four so the JSON roundtrip doesn't silently
drop fields.
Rendering for v1.4.7 covers Glow only: when Config.ShowHonorificGlow
is on and the title has a Glow colour, the chat header title gets an
8-direction ±1px draw-list outline pre-pass in the glow colour at 0.4
alpha, then the primary text on top.
Gradient (Color3 / GradientColourSet / GradientAnimationStyle) is parsed
and stashed for a later cycle — porting the full animation needs
Honorific's hardcoded Pride-palette list and GradientSystem.cs (or an
upstream IPC PR exposing the resolved frame colour). Tracked as
"Honorific Full Gradient Port" in the vault backlog.
ShowHonorificGlow defaults OFF — keeps v1.4.6 visuals untouched and
dodges per-frame DrawList overhead on low-end hardware. Tooltip flags
the gradient deferral so users aren't surprised by static rendering.
Tester-Request from Jin (2026-05-03): TempTabs should be pinnable so a
key conversation partner survives a relog. Right-click a TempTab and
choose Pin Tab / Unpin Tab / Promote to permanent.
Pool semantics:
- AutoTellTabsLimit (15) still gates the auto-managed unpinned pool.
- Pinned TempTabs live in their own pool, hard-capped at 5.
- The 6th pin attempt fails with a notification; users can unpin first
or promote to permanent.
- Unpinning into a full unpinned pool drops the oldest unpinned (no
user friction).
Mechanics:
- Tab.IsPinned (default false); Tab.Clone() carries it.
- Migration v16 -> v17 (additive; existing tabs default to unpinned).
- Three strip-sites synchronised through TabLifecycleHelpers:
Plugin.cs load-time, Plugin.SaveConfig, Configuration.UpdateFrom.
- AutoTellTabsService:
* MaxPinnedTempTabs constant.
* F2.1 _activeTempTabCount counter retired — ActiveTempTabCount is
now Tabs.Count(predicate). Pin/Unpin/Promote transitions are
cold-path and don't need lock-free reads.
* DropOldestTempTab filters on IsInUnpinnedPool so pinned tabs are
never drop candidates.
* OnLogout strips only the unpinned pool; pinned popouts and the
active-tab switch behave correspondingly.
* TryPin / Unpin / PromoteToPermanent service methods.
- ChatLogWindow tab context menu: Pin / Unpin / Promote with disabled-
state at-cap tooltip + Promote tooltip explaining the channel-filter
side effect.
- HellionStrings (EN+DE) for menu labels, tooltips, the limit warning.
- AutoTellTabsLimit slider description now flags the separate pinned
pool so users aren't surprised by 18 tabs when the limit reads 15.
Cherry-pick from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12).
Chat.SetChannel allocates a native Utf8String for the target name and
then runs a validity check. The previous early return on an invalid
linkshell skipped Dtor and leaked the native allocation; every invalid
linkshell switch added one Utf8String to the unmanaged heap.
- Renamed ValidAnyLinkshell to IsChannelOrExistingLinkshell so the
call-site reads naturally.
- Wrapped ChangeChatChannel in the validity check instead of
early-returning. Dtor now runs on every path.
- ChatLogWindow follows the rename at its single call-site.
The 2px underline pill was hardcoded — at 125/150% DPI the surrounding
tab layout scaled with ImGuiHelpers.GlobalScale but the pill stayed
2px, so the line landed on sub-pixel boundaries and rendered as a
fuzzy band. Now: height scales with GlobalScale (clamped to >=1px),
and the DrawList coordinates round to physical pixels via MathF.Round
so the rect aligns with the framebuffer grid.
Pattern-adherence pass after the cycle's code commits:
- ChatLogWindow.cs: NotifiedDrawFailure renamed from
_notifiedDrawFailure. The file's per-window state flags (DrewThisFrame,
WasDocked, Activate, PlayedClosingSound, …) all use PascalCase
without underscore prefix; the new flag now matches that
- Plugin.cs: trim the session-only RemoveAll comment from 5 lines to 2
and add the standard TEST-MIRROR pointer line. Same shape as
AutoTellTabsService.cs:28 and the other six TEST-MIRROR sites
- InputHistoryService.cs: add the TEST-MIRROR pointer for the new
Build-Suite tests
Surfaces a per-session warning notification when DrawChatLog throws so
the user knows something went wrong instead of staring at an empty
window. Stack trace stays in /xllog as before. The one-shot guard
prevents the notification stack from flooding frame-by-frame; it
resets only on the next plugin reload.
- Translated project documentation (LEARNING-JOURNEY, CONTRIBUTORS, AI_DISCLOSURE) to English for better accessibility.
- Standardized internal code documentation by converting XML-doc blocks to standard comment format.
- Cleaned up inline comments and removed redundant versioning metadata across the codebase.
- Refactored non-functional text elements to improve readability and maintain a consistent style.
Add .prettierrc.json, .markdownlint.json, .yamllint.yaml, .gitattributes
Run CSharpier, Prettier and markdownlint across the entire codebase.
No logic changes — formatting, using order and line endings only.
UI:
- SettingsOverview cards now wrap subtext to two lines (DrawList wrap-
width) and the card height grew from 96 to 110 px. Single-line
fitting clipped most of the bilingual subtitles.
- HellionStyle pushes ChildBg with alpha 0 when WindowOpacity < 1.0
to keep stacked BeginChild layers from compounding the deckgrade
past what the slider suggests.
- WindowOpacity slider helpmarker now points to Dalamud's per-window
hamburger menu for opacity / blur / pin / click-through overrides.
UX defaults (v15 → v16 migration adopts new values only when the user
is still on the previous default — bool flips are heuristic, the prior
defaults are from the v1.2.0 cycle and rarely toggled):
- UseCompactDensity false → true (single-line message style is cleaner)
- HideInNewGamePlusMenu false → true (consistent with other hide-flags)
- HideSameTimestamps false → true (cleaner log)
- MaxLinesToRender 5000 → 2500 (mid-range hardware friendlier)
- ChatColours empty → Hellion brand preset (the first-run wizard does
not offer a preset choice, so fresh installs get the brand colours
out of the box)
Sin-based alpha scaling between 60% and 100% with a 2-second cycle.
Subtle enough to register peripherally without becoming distracting.
Respects Plugin.Config.ReduceMotion (field exists since v1.1.0,
toggle UI lands in v1.3.0) — static render when disabled.
The sidebar child window's ChildBg painted the upper top-padding area
(reserved for header-toolbar alignment) with the theme's frame color,
making it look like a stub block above the buttons. Pushing ChildBg
to transparent keeps the buttons floating on the window background.
Vertical separation to the message column stays intact via the
TabTable's BordersInnerV flag.
Sidebar buttons sat at the window top while messages began below the
chat header toolbar — vertical mismatch flagged by Flo. Adding a
GetFrameHeightWithSpacing dummy at the top of the sidebar child shifts
the entire button column down to align with the first message row.
Reverted the previous TextLineHeight+4f button shrink (commit 8a78390):
buttons size was fine, only their vertical position needed correction.