diff --git a/HellionChat/GameFunctions/KeybindManager.cs b/HellionChat/GameFunctions/KeybindManager.cs index ec7c7ea..e5ee236 100644 --- a/HellionChat/GameFunctions/KeybindManager.cs +++ b/HellionChat/GameFunctions/KeybindManager.cs @@ -501,20 +501,40 @@ internal unsafe class KeybindManager : IDisposable return; Plugin.KeyState[currentBest.Item1] = false; - if (!KeybindsToIntercept.ContainsKey(currentBest.Item2)) + if (!KeybindsToIntercept.TryGetValue(currentBest.Item2, out var info)) return; // Re-surface the chat-activation entry point retired in v1.6.0: a chat-open // keybind shows + focuses the window, restoring it from a user-hide or a - // closed state. Channel/prefill routing from the bind stays out of scope. + // closed state. Plugin.Instance.MainWindow?.ActivateChat(); + + // Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch the + // game channel AND mirror it onto the active tab so the input pill shows the + // real send target (pill-sync, Flo decision 2026-06-15). Rotation binds (REPLY / + // linkshell-cycle, Rotate != None) and the Permanent nuance stay deferred to the + // keybind-routing follow-cycle. + if (info.Channel is { } channel && info.Rotate == RotateMode.None) + { + Plugin.Instance.Functions.Chat.SetChannel(channel); + if (Plugin.Instance.MainWindow?.ActiveTab is { } activeTab) + { + activeTab.CurrentChannel.SetChannel(channel); + activeTab.CurrentChannel.TellTarget = null; + activeTab.CurrentChannel.ResetTempChannel(); + } + } + + // Prefill text binds (CMD_COMMAND seeds "/"): drop the token into our input. + if (info.Text is { } text) + Plugin.Instance.InputBar.SetPendingMessage(text); } - // Tab-cycle dispatch is offline until the new chat layer surfaces a - // ChangeTabDelta entry point and pop-out input bars come back online. + // Cycle the main window's active tab. Pop-out input-bar focus-forward stays + // deferred (no focus contract yet) — main-window tabs only. private void DispatchTabDelta(int delta) { - _ = delta; + Plugin.Instance.MainWindow?.ChangeTabDelta(delta); } private static Keybind GetKeybind(string id) diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 646a9e9..59f095f 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -160,6 +160,25 @@ internal sealed class MainWindow : Window TabLifecycleHelpers.OnTabActivated(tab, previous); } + // Tab-cycle entry point for the ChatTabForward/Backward keybinds. Empty list is a + // no-op; a null active tab seeds tabs[0]; a single-tab cycle that lands on the + // already-active tab is a no-op (ActivateTab early-returns on the same reference). + // Routes through ActivateTab so the cycle strips stale tell state + re-derives the + // channel exactly like a sidebar/top-tab click. Pop-out focus-forward stays + // deferred (no focus contract) — main-window tabs only. + internal void ChangeTabDelta(int delta) + { + var tabs = Plugin.Config.Tabs; + if (tabs.Count == 0) + return; + + var idx = _activeTab is null ? 0 : tabs.IndexOf(_activeTab); + if (idx < 0) + idx = 0; // active tab not in the list (mid-strip) -> start from the first + + ActivateTab(tabs[TabLifecycleHelpers.WrapTabIndex(idx, delta, tabs.Count)]); + } + // Internal accessors for self-tests so the probes can reach the live // component without exposing them as public surface. internal Components.Sidebar GetSidebarForSelfTest() => _sidebar; diff --git a/HellionChat/Util/TabLifecycleHelpers.cs b/HellionChat/Util/TabLifecycleHelpers.cs index 9c2a6f8..e9edc6f 100644 --- a/HellionChat/Util/TabLifecycleHelpers.cs +++ b/HellionChat/Util/TabLifecycleHelpers.cs @@ -83,4 +83,15 @@ internal static class TabLifecycleHelpers tab.CurrentChannel.TellTarget = null; tab.CurrentChannel.ResetTempChannel(); } + + // Wrap-around tab index for keybind cycling. Pure so the Build-Suite can test the + // wrap math without a live window. count == 0 returns 0 (the caller dead-zones + // before activating); negative deltas wrap correctly via the double-mod. + // TEST-MIRROR: ../../Hellion Build test/_Helpers/TabLifecycleHelpersTests.cs + internal static int WrapTabIndex(int current, int delta, int count) + { + if (count <= 0) + return 0; + return ((current + delta) % count + count) % count; + } }