MessageStore, MessageEnumerator, MessageManager, AutoTellTabsService
move from Plugin.LogProxy / IPluginLogProxy onto
Microsoft.Extensions.Logging.ILogger<T> via constructor injection.
MessageStore additionally takes ILoggerFactory so it can build a
per-instance ILogger<MessageEnumerator> at each of the five reader-
spawning sites; the enumerator is not a container singleton.
PluginHostFactory's MessageManager and AutoTellTabsService factory
lambdas grow to resolve the new logger args; everything else stays in
place.
Site-level migration in the four files:
- MessageStore: 12 calls, _logger field IPluginLogProxy -> ILogger<MessageStore>
- MessageManager: 7 Plugin.LogProxy.* sites, new _logger field
- AutoTellTabsService: 9 Plugin.LogProxy.* sites, new _logger field
Plus a pre-existing template bug surfaced by CA2017: a LogDebug call
in AutoTellTabsService used "{tab.Name}" with no `$` prefix, which
landed in xllog as literal text under Plugin.LogProxy; ILogger now
reads that as a structured placeholder, so the call was promoted to
proper structured logging with tab.Name passed as a parameter.
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.
Smoke-test (Jin's scenario) surfaced two coupled bugs in v1.4.7 pin
persistence:
1. The chat input couldn't send to the pinned partner after a reload.
Tab.CurrentChannel is NonSerialized, so it came back as a fresh
UsedChannel with TellTarget=null even though tab.TellTarget (the
persisted twin) was intact. The game-side channel hook only repaints
CurrentChannel on a /tell or channel switch, so the pinned tab sat
there mute until the user manually re-bounced the channel.
2. An incoming tell from the pinned partner spawned a *second* TempTab
instead of routing into the existing pinned one. The Name+World
lookup in FindTempTab was vulnerable to any round-trip nuance on
tab.TellTarget — the fallback path now matches by tab name, which
FormatTabName pins at spawn time.
Fix:
- AutoTellTabsService.Initialize now calls RehydratePinnedTabs() after
the Phase-2 wiring lands, seeding tab.CurrentChannel.TellTarget +
Channel from the persisted tab.TellTarget. Channel is also defaulted
to InputChannel.Tell on the tab record so the chat-input bar paints
Tell mode immediately on first selection.
- FindTempTab gained a Name-based fallback for the case where the
primary TellTarget lookup misses (e.g. a pinned tab whose TellTarget
didn't round-trip cleanly through an old save).
- HandleTell self-heals: when the fallback matches a pinned tab with a
missing TellTarget, the tab is repaired from the live partner data
and persisted, so subsequent messages take the fast path.
Build-suite coverage was attempted (PinnedTabJsonRoundtripTests) but
Tab + TellTarget are both Dalamud-coupled — Newtonsoft's reflection
walk loads Dalamud.dll which isn't available in the xUnit AppDomain
(documented in feedback_dalamud_test_isolation). Verification stays on
the ingame smoke path.
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.
F2.1: ActiveTempTabCount was doing a LINQ Count under _tempTabsLock on
every read, including the hot-path HandleTell guard. Replace with an
Interlocked counter kept in sync with Config.Tabs from inside the
existing mutation paths (SpawnTempTab, DropOldestTempTab, OnLogout).
Initialize from the persisted Tabs list on Initialize() to handle
configs that already contain TempTabs from a prior session.
Plugin.cs SaveConfig snapshot-restore mutates Config.Tabs outside of
AutoTellTabsService; expose ResyncTempTabCounter() and call it after
AddRange so the counter stays consistent. Plugin.cs:168 crash-recovery
RemoveAll runs before Initialize() and is covered by the init snapshot.
Updated .editorconfig to set indent_style=space and indent_size=4 for C# files. Reformat all .cs files to apply the new indentation settings. No code logic changes, just whitespace reformatting.
also updated some comments in files in shorter and Precise way. No logic changes, just comment rewording for clarity and conciseness.
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.