acdream/src/AcDream.Headless/Hosting/HeadlessConsoleSpewBoxPump.cs
Erik 3328b2f2b3 refactor(headless): S5 poll SpewBox on the console pump instead of decorating one call
HeadlessConsoleChatFeedback wrapped RuntimeChatCommandFeedback per
SubmitConsoleLine call, so it only ever saw interface text produced by
the console's OWN typed line -- a server-driven refusal or a plugin's
own Log/interface-text write (RuntimeCommunicationState.AddText's
ClientLocal branch, called from anywhere else) never reached the
console at all, because that branch enqueues into SpewBoxState and
never touches ChatLog/RuntimeChatDelta.

Delete the decorator. HeadlessConsoleSpewBoxPump instead polls the SAME
SpewBoxState the graphical overlay's SpewBoxController.Tick already
reads, diffing against the previous visible snapshot so it prints only
newly-appeared entries. HeadlessProcessHost's ConsolePump now runs the
input drain and the SpewBox pump together each tick.
HeadlessSessionHost.SubmitConsoleLine drops its onInterfaceText
parameter -- it is just ChatCommandRouter.Submit against a plain
RuntimeChatCommandFeedback now, same as LoginCommandSequence.

N1: also corrected this method's own doc comment, which described
dispatch as "plugin verb registry first, then retail client/server
slash commands" -- the real ChatCommandRouter.Submit order is retail's
catalog, local /help, plugin verbs, the channel-tag fallback, an
explicit server command, then chat.

PumpPrintsInterfaceTextNotOriginatingFromTheConsole was shown to fail
against a no-op Pump() (mutation) -- the enqueued plugin-shaped line
never printed. UnknownVerbProducesTheSameInterfaceTextTheChatBoxShows
was reworked to assert against the real SpewBoxState directly instead
of the deleted decorator's callback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:01:24 +02:00

63 lines
2.7 KiB
C#

using AcDream.Core.Chat;
namespace AcDream.Headless.Hosting;
/// <summary>
/// S5 (2026-09-07 review round, docs/plans/2026-09-07-headless-console.md):
/// polls <see cref="SpewBoxState"/> on the console's own per-tick pump — the
/// SAME seam <c>AcDream.App.UI.SpewBoxController.Tick</c> drives for the
/// graphical overlay. Retail's transient "interface text"
/// (<see cref="RetailLogTextType.ClientLocal"/>, routed by
/// <c>RuntimeCommunicationState.AddText</c>) never touches
/// <c>RuntimeCommunicationState.Chat</c>/<c>RuntimeChatDelta</c>, so it is
/// otherwise invisible to a console that only observes the chat event
/// stream — this is true for EVERY producer of that text (a bad-args
/// refusal from the console's own submit, but also a server-driven refusal
/// or a plugin's own interface-text write), not just the console's own
/// submissions. This replaces the earlier per-call
/// <c>HeadlessConsoleChatFeedback</c> decorator, which only ever saw text
/// produced by the console's own <c>SubmitConsoleLine</c> calls.
/// </summary>
internal sealed class HeadlessConsoleSpewBoxPump
{
private readonly SpewBoxState _spewBox;
private readonly Func<double> _nowSeconds;
private readonly Action<string> _writeInterfaceText;
private SpewBoxEntry[] _lastSeen = [];
internal HeadlessConsoleSpewBoxPump(
SpewBoxState spewBox,
Func<double> nowSeconds,
Action<string> writeInterfaceText)
{
_spewBox = spewBox ?? throw new ArgumentNullException(nameof(spewBox));
_nowSeconds = nowSeconds
?? throw new ArgumentNullException(nameof(nowSeconds));
_writeInterfaceText = writeInterfaceText
?? throw new ArgumentNullException(nameof(writeInterfaceText));
}
/// <summary>
/// Drains any pending SpewBox text into the visible set (exactly
/// <see cref="SpewBoxState.Tick"/>'s contract — the same drain
/// <c>SpewBoxVM.Lines</c> performs for the graphical overlay) and prints
/// any entry that was not part of the previous call's visible snapshot.
/// </summary>
/// <remarks>
/// <see cref="SpewBoxState.Snapshot"/> is newest-first
/// (retail's <c>InsertItem(item, 0)</c>); this walks it back-to-front so
/// newly-visible entries print in the order they were actually
/// enqueued, not newest-first.
/// </remarks>
internal void Pump()
{
_spewBox.Tick(_nowSeconds());
SpewBoxEntry[] current = _spewBox.Snapshot();
for (int i = current.Length - 1; i >= 0; i--)
{
if (Array.IndexOf(_lastSeen, current[i]) < 0)
_writeInterfaceText(current[i].Text);
}
_lastSeen = current;
}
}