feat(ipc): wire TypingIpc state from InputBar API
This commit is contained in:
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>>()
|
||||
));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user