@@ -79,6 +79,10 @@ type MessageMonitor struct {
7979 // Initial subject (for auto-subscribe on Start)
8080 initSubject string
8181
82+ // When set, the monitor watches this entire stream (all subjects) instead
83+ // of a single subject filter. The subject input becomes a client-side filter.
84+ streamName string
85+
8286 // JSON path / pipeline filter for preview pane
8387 jsonFilter string
8488 pipeline * Pipeline
@@ -89,6 +93,24 @@ func NewMessageMonitor(app *App) *MessageMonitor {
8993 return NewMessageMonitorWithSubject (app , "" )
9094}
9195
96+ // NewMessageMonitorForStream creates a monitor that watches an entire stream
97+ // (every subject it stores), rather than a single subject filter. The subject
98+ // field acts as a client-side filter over the live messages instead of
99+ // changing the subscription.
100+ func NewMessageMonitorForStream (app * App , streamName string ) * MessageMonitor {
101+ mm := & MessageMonitor {
102+ app : app ,
103+ useJetStream : true ,
104+ deliverPolicy : nats .DeliverAll ,
105+ streamName : streamName ,
106+ processCancel : func () {},
107+ }
108+
109+ mm .buildUI ("" )
110+ mm .setupBinding ()
111+ return mm
112+ }
113+
92114// NewMessageMonitorWithSubject creates the message monitor with a pre-filled subject.
93115func NewMessageMonitorWithSubject (app * App , subject string ) * MessageMonitor {
94116 mm := & MessageMonitor {
@@ -123,16 +145,30 @@ func (mm *MessageMonitor) buildUI(subject string) {
123145 SetWordWrap (true )
124146 mm .preview .SetBackgroundColor (theme .Bg ())
125147
126- // Subject input
148+ // Subject input. In stream-watch mode it filters the live list client-side
149+ // rather than re-subscribing, since the subscription already covers the
150+ // whole stream.
151+ subjectLabel := "Subject: "
152+ subjectPlaceholder := ">"
153+ if mm .streamName != "" {
154+ subjectLabel = "Filter: "
155+ subjectPlaceholder = "filter subjects..."
156+ }
127157 mm .subjectInput = components .NewTextField ("subject" ).
128- SetLabel ("Subject: " ).
129- SetPlaceholder (">" )
158+ SetLabel (subjectLabel ).
159+ SetPlaceholder (subjectPlaceholder )
130160 mm .subjectInput .SetBackgroundColor (theme .Bg ())
131161 mm .subjectInput .SetOnSubmit (func (_ * components.SubmitEvent ) {
132- subj := mm .subjectInput .GetValue ()
133- // subscribe is goroutine-safe; dispatch off main goroutine so
134- // the NATS handshake cannot block the event loop.
135- go mm .subscribe (subj )
162+ val := mm .subjectInput .GetValue ()
163+ // Move focus off the text input so arrow keys navigate the message
164+ // list instead of being captured by the field.
165+ mm .app .app .SetFocus (mm .MasterDetailView )
166+ // Dispatch off the main goroutine so the work can't block the event loop.
167+ if mm .streamName != "" {
168+ go mm .setFilter (val )
169+ return
170+ }
171+ go mm .subscribe (val )
136172 })
137173 if subject != "" {
138174 mm .subjectInput .SetValue (subject )
@@ -237,6 +273,12 @@ func (mm *MessageMonitor) Start() {
237273 // Start() runs on main goroutine — safe to touch UI directly.
238274 atomic .StoreInt32 (& mm .stopped , 0 )
239275
276+ // Whole-stream watch: subscribe to the entire stream regardless of subject.
277+ if mm .streamName != "" {
278+ go mm .subscribe ("" )
279+ return
280+ }
281+
240282 // Re-subscribe to whatever subject was active (or the initial one).
241283 subject := mm .state .Get ().subject
242284 if subject == "" {
@@ -434,10 +476,23 @@ func (mm *MessageMonitor) HandleKey(event *tcell.EventKey) bool {
434476// ── Subscribe / Unsubscribe ────────────────────────────────────────────────
435477
436478func (mm * MessageMonitor ) subscribe (subject string ) {
437- if subject == "" || atomic .LoadInt32 (& mm .stopped ) == 1 {
479+ if atomic .LoadInt32 (& mm .stopped ) == 1 {
438480 return
439481 }
440482
483+ // Whole-stream watch mode: no subject filter, drive off the stream name.
484+ streamMode := mm .streamName != "" && subject == ""
485+ if subject == "" && ! streamMode {
486+ return
487+ }
488+
489+ // Label used for display/state (the subscription itself is by stream in
490+ // stream mode, so there is no single subject to show).
491+ displayLabel := subject
492+ if streamMode {
493+ displayLabel = fmt .Sprintf ("%s (all subjects)" , mm .streamName )
494+ }
495+
441496 // Tear down any existing subscription first.
442497 mm .doUnsubscribe ()
443498
@@ -461,15 +516,15 @@ func (mm *MessageMonitor) subscribe(subject string) {
461516 // Push "subscribing" state.
462517 mm .state .SetAndDraw (monitorState {
463518 status : statusSubscribing ,
464- subject : subject ,
519+ subject : displayLabel ,
465520 mode : mm .modeLabel (),
466521 policy : mm .policyLabel (),
467522 })
468523
469524 // Start processor goroutine for this subscription.
470525 processCtx , processCancel := context .WithCancel (context .Background ())
471526 mm .processCancel = processCancel
472- go mm .processMessages (processCtx , myGen , msgChan , subject )
527+ go mm .processMessages (processCtx , myGen , msgChan , displayLabel )
473528
474529 // Handler: never blocks, never holds locks. Channel send is guarded by
475530 // recover so a closed-channel panic in the narrow unsubscribe race is safe.
@@ -489,16 +544,21 @@ func (mm *MessageMonitor) subscribe(subject string) {
489544 var err error
490545 var modeLabel string
491546
492- if useJS {
547+ if streamMode {
548+ // Whole-stream watch is inherently JetStream — no single subject for
549+ // a core NATS subscription to bind to.
550+ sub , err = provider .SubscribeJetStreamStream (context .Background (), mm .streamName , policy , handler )
551+ modeLabel = "JetStream"
552+ } else if useJS {
493553 sub , err = provider .SubscribeJetStream (context .Background (), subject , policy , handler )
494554 modeLabel = "JetStream"
495555 } else {
496556 sub , err = provider .Subscribe (context .Background (), subject , handler )
497557 modeLabel = "Core NATS"
498558 }
499559
500- // Fallback to core NATS if JetStream fails.
501- if err != nil && useJS {
560+ // Fallback to core NATS if JetStream fails (single-subject mode only) .
561+ if err != nil && useJS && ! streamMode {
502562 sub , err = provider .Subscribe (context .Background (), subject , handler )
503563 if err == nil {
504564 modeLabel = "Core NATS (fallback)"
@@ -511,7 +571,7 @@ func (mm *MessageMonitor) subscribe(subject string) {
511571 if err != nil {
512572 mm .state .SetAndDraw (monitorState {
513573 status : statusError ,
514- subject : subject ,
574+ subject : displayLabel ,
515575 errMsg : err .Error (),
516576 mode : mm .modeLabel (),
517577 policy : mm .policyLabel (),
@@ -533,7 +593,7 @@ func (mm *MessageMonitor) subscribe(subject string) {
533593 mm .mu .Unlock ()
534594
535595 mm .app .QueueUpdateDraw (func () {
536- mm .app .ShowSuccess (fmt .Sprintf ("Subscribed to %s (%s)" , subject , modeLabel ))
596+ mm .app .ShowSuccess (fmt .Sprintf ("Subscribed to %s (%s)" , displayLabel , modeLabel ))
537597 })
538598}
539599
0 commit comments