@@ -21,6 +21,7 @@ import {
2121 MudPromptManager ,
2222 MudSocketAdapter ,
2323 MudPromptContext ,
24+ MudScreenReaderAnnouncer ,
2425} from '../../../../features/terminal' ;
2526
2627/**
@@ -37,7 +38,9 @@ const DELETE_SEQUENCE = `${CTRL.ESC}[3~`;
3738
3839/**
3940 * Angular wrapper around the xterm-based MUD client. The component hosts the terminal,
40- * wires the input/prompt helpers together and mirrors socket events to the view.
41+ * wires the input/prompt helpers together and mirrors socket events to the view. A
42+ * custom screenreader announcer replaces xterm's built-in screenReaderMode to avoid
43+ * duplicated output and replaying history after reconnects.
4144 */
4245@Component ( {
4346 selector : 'app-mud-client' ,
@@ -52,19 +55,10 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
5255 private readonly terminal : Terminal ;
5356 private readonly inputController : MudInputController ;
5457 private readonly promptManager : MudPromptManager ;
58+ private screenReader ?: MudScreenReaderAnnouncer ;
5559 private readonly terminalFitAddon = new FitAddon ( ) ;
56- private readonly socketAdapter = new MudSocketAdapter (
57- this . mudService . mudOutput$ ,
58- {
59- transformMessage : ( data ) => this . transformMudOutput ( data ) ,
60- beforeMessage : ( data ) => this . beforeMudOutput ( data ) ,
61- afterMessage : ( data ) => this . afterMudOutput ( data ) ,
62- } ,
63- ) ;
64- private readonly terminalAttachAddon = new AttachAddon (
65- this . socketAdapter as unknown as WebSocket ,
66- { bidirectional : false } ,
67- ) ;
60+ private socketAdapter ?: MudSocketAdapter ;
61+ private terminalAttachAddon ?: AttachAddon ;
6862
6963 private readonly terminalDisposables : IDisposable [ ] = [ ] ;
7064 private readonly resizeObs = new ResizeObserver ( ( ) => {
@@ -84,6 +78,12 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
8478 @ViewChild ( 'hostRef' , { static : true } )
8579 private readonly terminalRef ! : ElementRef < HTMLDivElement > ;
8680
81+ @ViewChild ( 'liveRegionRef' , { static : true } )
82+ private readonly liveRegionRef ! : ElementRef < HTMLDivElement > ;
83+
84+ @ViewChild ( 'historyRegionRef' , { static : true } )
85+ private readonly historyRegionRef ! : ElementRef < HTMLElement > ;
86+
8787 protected readonly isConnected$ = this . mudService . connectedToMud$ ;
8888 protected readonly showEcho$ = this . mudService . showEcho$ ;
8989
@@ -96,7 +96,7 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
9696 fontFamily : 'JetBrainsMono, monospace' ,
9797 theme : { background : '#000' , foreground : '#ccc' } ,
9898 disableStdin : false ,
99- screenReaderMode : true ,
99+ screenReaderMode : false ,
100100 } ) ;
101101
102102 this . inputController = new MudInputController (
@@ -116,6 +116,28 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
116116 * to socket events and reports the initial viewport dimensions to the server.
117117 */
118118 ngAfterViewInit ( ) {
119+ // Initialize screenreader announcer before terminal/socket setup
120+ // to ensure we capture the session start BEFORE any output arrives
121+ this . screenReader = new MudScreenReaderAnnouncer (
122+ this . liveRegionRef . nativeElement ,
123+ this . historyRegionRef . nativeElement ,
124+ ) ;
125+ console . debug (
126+ '[MudClient] Screenreader announcer initialized, live region:' ,
127+ this . liveRegionRef . nativeElement ,
128+ ) ;
129+
130+ // Now initialize socket adapter AFTER screenreader is ready
131+ this . socketAdapter = new MudSocketAdapter ( this . mudService . mudOutput$ , {
132+ transformMessage : ( data ) => this . transformMudOutput ( data ) ,
133+ beforeMessage : ( data ) => this . beforeMudOutput ( data ) ,
134+ afterMessage : ( data ) => this . afterMudOutput ( data ) ,
135+ } ) ;
136+ this . terminalAttachAddon = new AttachAddon (
137+ this . socketAdapter as unknown as WebSocket ,
138+ { bidirectional : false } ,
139+ ) ;
140+
119141 this . terminal . open ( this . terminalRef . nativeElement ) ;
120142 this . terminal . loadAddon ( this . terminalFitAddon ) ;
121143 this . terminal . loadAddon ( this . terminalAttachAddon ) ;
@@ -152,15 +174,17 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
152174 this . showEchoSubscription ?. unsubscribe ( ) ;
153175 this . linemodeSubscription ?. unsubscribe ( ) ;
154176
155- this . terminalAttachAddon . dispose ( ) ;
156- this . socketAdapter . dispose ( ) ;
177+ this . terminalAttachAddon ? .dispose ( ) ;
178+ this . socketAdapter ? .dispose ( ) ;
157179 this . terminal . dispose ( ) ;
180+ this . screenReader ?. dispose ( ) ;
158181 }
159182
160183 protected connect ( ) {
161184 const columns = this . terminal . cols ;
162185 const rows = this . terminal . rows ;
163186
187+ this . screenReader ?. markSessionStart ( ) ;
164188 this . mudService . connect ( { columns, rows } ) ;
165189 }
166190
@@ -203,6 +227,10 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
203227 ? message
204228 : { value : message } ;
205229
230+ if ( typeof payload === 'string' ) {
231+ this . screenReader ?. appendToHistory ( payload ) ;
232+ }
233+
206234 this . mudService . sendMessage ( payload ) ;
207235 }
208236
@@ -272,6 +300,8 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
272300 */
273301 private afterMudOutput ( data : string ) {
274302 this . promptManager . afterServerOutput ( data , this . getPromptContext ( ) ) ;
303+ this . announceToScreenReader ( data ) ;
304+ this . screenReader ?. appendToHistory ( data ) ;
275305 }
276306
277307 /**
@@ -292,6 +322,23 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
292322 } ;
293323 }
294324
325+ /**
326+ * Announces new server output via the custom screenreader announcer.
327+ * Called AFTER prompt restoration so we announce the final visible text.
328+ */
329+ private announceToScreenReader ( data : string ) : void {
330+ if ( ! this . screenReader ) {
331+ return ;
332+ }
333+
334+ console . debug ( '[MudClient] Announcing to screenreader:' , {
335+ rawLength : data . length ,
336+ raw : data ,
337+ } ) ;
338+
339+ this . screenReader . announce ( data ) ;
340+ }
341+
295342 /**
296343 * Convenience helper for patching the local state object.
297344 */
0 commit comments