diff --git a/src/AcDream.App/Plugins/AppAutomationSurface.cs b/src/AcDream.App/Plugins/AppAutomationSurface.cs
index a4f87d0b..9ae412f7 100644
--- a/src/AcDream.App/Plugins/AppAutomationSurface.cs
+++ b/src/AcDream.App/Plugins/AppAutomationSurface.cs
@@ -1170,9 +1170,12 @@ internal sealed class AppAutomationSurface
}
///
- /// Routed to retail's ClientLocal log type (0x1A) — the channel the client
- /// uses for its own notices. Nothing reaches the server, so a plugin cannot
- /// accidentally speak in the player's name.
+ /// Owner direction 2026-09-07 (register row AD-124): plugin-originated
+ /// text now lands in the chat window (retail Default/0x00),
+ /// matching Decal's own AddChatText behavior — not retail's
+ /// ClientLocal (0x1A) SpewBox-only channel this previously used.
+ /// Nothing reaches the server, so a plugin cannot accidentally speak in
+ /// the player's name.
///
public void PostSystemMessage(string text)
{
@@ -1181,7 +1184,7 @@ internal sealed class AppAutomationSurface
RuntimeCommunicationState? communication;
lock (_gate)
communication = _communication;
- communication?.AddText(text, RetailLogTextType.ClientLocal);
+ communication?.AddText(text, RetailLogTextType.Default);
}
public bool Submit(string text)
diff --git a/src/AcDream.Plugin.Abstractions/Automation.cs b/src/AcDream.Plugin.Abstractions/Automation.cs
index 894d062b..2e968d1e 100644
--- a/src/AcDream.Plugin.Abstractions/Automation.cs
+++ b/src/AcDream.Plugin.Abstractions/Automation.cs
@@ -302,10 +302,20 @@ public interface IPluginChat
Array.Empty();
///
- /// Post a client-local system line, the channel retail uses for the
- /// client's own notices. It is local to this client: nothing is sent to the
- /// server and no other player sees it.
+ /// Post a plugin-originated system line into the chat window. It is
+ /// local to this client: nothing is sent to the server and no other
+ /// player sees it.
///
+ ///
+ /// Owner direction 2026-09-07 (register row AD-124): this used to route
+ /// through retail's ClientLocal (0x1A) channel — the SpewBox
+ /// overlay every ChatInterface window's default filter excludes.
+ /// The owner explicitly overrode that for plugin text, matching Decal's
+ /// own AddChatText behavior: plugin output now lands in the chat
+ /// transcript (retail Default/0x00) so it is actually visible and
+ /// scrolls back, never the transient overlay. See
+ /// AppAutomationSurface.PostSystemMessage for the implementation.
+ ///
void PostSystemMessage(string text);
///
diff --git a/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs b/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs
index 9982ace3..b3221fde 100644
--- a/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs
+++ b/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs
@@ -146,6 +146,31 @@ public sealed class AppAutomationSurfaceTests
Assert.Equal(0, second.CommunicationOwner.SubscriberCount);
}
+ ///
+ /// Owner-directed override 2026-09-07 (register row AD-124): plugin
+ /// output ("Unknown commands like /vt or stuff from plugins ... should
+ /// go to the chatbox") must land in the chat log, never the transient
+ /// SpewBox overlay retail's own ClientLocal (0x1A) typing used to send
+ /// it to — the same VTank-faithful destination Decal's own
+ /// AddChatText uses.
+ ///
+ [Fact]
+ public void PostSystemMessage_RoutesToChatLog_NeverSpewBox()
+ {
+ using var runtime = GameRuntimeTestFactory.Create();
+ using var surface = new AppAutomationSurface();
+ surface.Bind(runtime, runtime.CharacterOwner, runtime.ActionOwner.SpellCast);
+
+ surface.PostSystemMessage("MossTank: buffs applied.");
+
+ var entry = Assert.Single(runtime.CommunicationOwner.Chat.Snapshot());
+ Assert.Equal("MossTank: buffs applied.", entry.Text);
+ Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
+
+ runtime.CommunicationOwner.SpewBox.Tick(0d);
+ Assert.Equal(0, runtime.CommunicationOwner.SpewBox.Count);
+ }
+
[Fact]
public void InventoryCompletionProjectsTheCanonicalRequestReceipt()
{
diff --git a/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs
index 15cb0cc5..770aae9c 100644
--- a/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs
@@ -194,4 +194,36 @@ public sealed class ChatVMTests
// The stored body never carries the stamp in either state.
Assert.Equal("hi", log.Snapshot()[0].Text);
}
+
+ ///
+ /// Owner-directed override 2026-09-07 (register row AD-124): plugin
+ /// output (AppAutomationSurface.PostSystemMessage, the
+ /// production implementation of IPluginChat.PostSystemMessage)
+ /// now funnels into RuntimeCommunicationState.AddText(text,
+ /// RetailLogTextType.Default), which calls
+ /// Chat.OnSystemMessage(text, (uint)Default) — the exact call
+ /// this test performs directly on the shared ,
+ /// matching Decal's own AddChatText behavior for plugin text.
+ /// Any bound to that log (the production chat
+ /// window) must show the line; it must never depend on the
+ /// SpewBox seam, which this call
+ /// never touches.
+ ///
+ [Fact]
+ public void RecentLines_ShowsPluginSystemMessage_TaggedDefault()
+ {
+ var log = new ChatLog();
+ var vm = new ChatVM(log, displayLimit: 50);
+
+ log.OnSystemMessage(
+ "MossTank: buffs applied.",
+ chatType: (uint)RetailLogTextType.Default);
+
+ Assert.Equal(
+ "MossTank: buffs applied.",
+ Assert.Single(vm.RecentLines()));
+ Assert.Equal(
+ (uint)RetailLogTextType.Default,
+ Assert.Single(log.Snapshot()).LogTextType);
+ }
}