feat(ipc): wire TypingIpc state from InputBar API

This commit is contained in:
2026-05-26 13:38:21 +02:00
parent e01de0403a
commit 93bfd408bc
2 changed files with 38 additions and 10 deletions
+37 -10
View File
@@ -34,11 +34,13 @@ internal sealed class TypingIpc : IDisposable
private ChatInputState LastState;
private bool HasState;
private readonly Ui.Components.InputBar _inputBar;
private readonly ILogger<TypingIpc> _logger;
internal TypingIpc(Plugin plugin, ILogger<TypingIpc> logger)
internal TypingIpc(Plugin plugin, Ui.Components.InputBar inputBar, ILogger<TypingIpc> logger)
{
Plugin = plugin;
_inputBar = inputBar;
_logger = logger;
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
@@ -62,26 +64,51 @@ internal sealed class TypingIpc : IDisposable
private ChatInputState BuildState()
{
// Input visibility and focus come back when the new chat layer
// exposes the matching state. The channel type still resolves
// from the active tab so IPC consumers can read it today.
var usedChannel = Plugin.CurrentTab.CurrentChannel;
var inputChannel = usedChannel.UseTempChannel
? usedChannel.TempChannel
: usedChannel.Channel;
var channelType = inputChannel.ToChatType();
// `Plugin` here is the instance property on TypingIpc (TypingIpc.cs:18
// `private Plugin Plugin { get; }`), NOT the type `HellionChat.Plugin`.
// C# resolves `Plugin.MainWindow` as `this.Plugin.MainWindow` because
// the instance property shadows the type name inside this class. Do
// NOT switch to a type-qualified read (CS0120 — `MainWindow` is an
// instance member, not a static one). MainWindow is resolved in Phase-1
// (Plugin.cs:295) and never re-assigned, so `this.Plugin.MainWindow`
// is non-null by the time TypingIpc.Update() runs (HostedServices only
// start after Phase-1). The `null!`-suppression on the MainWindow
// property would let us drop the `?.`, but a null-safe read costs
// nothing at runtime and shields against a theoretical pre-Phase-1
// caller (a future IPC-pull that fires before HostedServices start).
// Defense in depth, no behaviour change for the production path.
var mainWindowOpen = this.Plugin.MainWindow?.IsOpen ?? false;
// Stale-state guard: InputBar._isFocused is only written in DrawInputField,
// which only runs while MainWindow is open. After the user closes the
// window, _isFocused freezes on the last value. _pendingMessage has the
// same stale problem — it is only cleared in TrySend (successful send)
// or ClearBuffer (manual reset), so closing MainWindow with non-empty
// buffer leaves PendingLength frozen above zero. Without gating all
// four state fields on mainWindowOpen, Cross-Plugin-Subscribers would
// keep seeing InputFocused: true / HasText: true / IsTyping: true /
// TextLength: N even though no input field exists.
var inputFocused = mainWindowOpen && _inputBar.IsFocused;
var hasText = mainWindowOpen && _inputBar.PendingLength > 0;
var textLength = mainWindowOpen ? _inputBar.PendingLength : 0;
return (
InputVisible: false,
InputFocused: false,
HasText: false,
IsTyping: false,
TextLength: 0,
InputVisible: mainWindowOpen,
InputFocused: inputFocused,
HasText: hasText,
IsTyping: hasText,
TextLength: textLength,
ChannelType: channelType
);
}
private ChatInputState GetState() => BuildState();
internal ChatInputState GetState() => BuildState();
internal void Update()
{
+1
View File
@@ -103,6 +103,7 @@ internal static class PluginHostFactory
));
services.AddSingleton(sp => new TypingIpc(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<ILogger<TypingIpc>>()
));