feat(tabs): add IsPinned with separate pool and 5-tab cap

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.
This commit is contained in:
2026-05-13 09:06:14 +02:00
parent fee2459e73
commit fd5f970a8b
8 changed files with 222 additions and 66 deletions
+46
View File
@@ -2124,10 +2124,56 @@ public sealed class ChatLogWindow : Window
anyChanged = true;
}
if (tab.IsTempTab)
{
ImGui.Separator();
DrawPinControls(tab);
}
if (anyChanged)
Plugin.SaveConfig();
}
private void DrawPinControls(Tab tab)
{
var svc = Plugin.AutoTellTabsService;
if (svc == null)
return;
if (tab.IsPinned)
{
if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin))
{
svc.Unpin(tab);
ImGui.CloseCurrentPopup();
}
}
else
{
var atCap = svc.PinnedTempTabCount >= AutoTellTabsService.MaxPinnedTempTabs;
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPin, enabled: !atCap))
{
if (svc.TryPin(tab))
ImGui.CloseCurrentPopup();
}
if (atCap && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(
string.Format(
HellionStrings.PinTab_LimitReached,
AutoTellTabsService.MaxPinnedTempTabs
)
);
}
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPromote))
{
svc.PromoteToPermanent(tab);
ImGui.CloseCurrentPopup();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(HellionStrings.PinTab_PromoteTooltip);
}
internal readonly List<bool> PopOutDocked = [];
internal readonly HashSet<Guid> PopOutWindows = [];