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>
63 lines
2.7 KiB
C#
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;
|
|
}
|
|
}
|