1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Central output chokepoint for asynchronous messages that need to
//! reach the user but DON'T originate from the agent's own event
//! stream.
//!
//! Previously, off-stream messages (MCP server stderr, plugin
//! warnings, background-task lifecycle pings, etc.) reached the
//! user via inconsistent paths: some went through `tracing::warn!`
//! (which writes to plain stderr and paints over the alt-screen
//! UI), some called `renderer.write_line` directly from inside
//! deeply-nested task spawns (requiring `&mut Renderer` access in
//! places it shouldn't be), and some leaked control bytes through
//! sanitizers built for one specific source.
//!
//! This module owns ONE `tokio::sync::mpsc::UnboundedSender<Notification>`
//! as a process-global; producers send a typed `Notification` and
//! the UI event loop drains the channel with the same
//! `tokio::select!` arm shape as `ask_rx` / `question_rx` /
//! `lifecycle_rx`. The receiver path runs through the standard
//! `Renderer::write_line` pipeline — same wrapping, same theming,
//! same scroll behaviour — so a message from an MCP server reads
//! the same way as an agent error or a permission denial.
use crateLockExt;
use ;
use ;
/// One off-stream message destined for the chat area. Variants pick
/// the visual treatment (color + prefix); content is plain text
/// that has ALREADY been sanitized of escape sequences by the
/// producer.
/// Bounded channel capacity. A sustained MCP-server stderr flood
/// (buggy panic loop, hostile / compromised child) could otherwise
/// queue unboundedly and OOM dirge — review #4. 1024 is enough
/// headroom for legitimate burst-y log emissions, and `try_send`
/// drops on overflow so a runaway producer can't outpace the UI.
const NOTIF_CAP: usize = 1024;
/// Global sender. Installed at startup; replaceable so a fresh
/// `install()` (test harness, future UI restart) swaps in a live
/// sender — review #2. RwLock instead of OnceLock so producers see
/// the LIVE sender, not an orphan bound to a dropped receiver.
///
/// Read path is hot (every MCP log line takes a read lock); write
/// path fires once at install. `RwLock` gives the right contention
/// shape.
static TX: = new;
/// Holding pen for the receiver between `install()` (called in
/// `main()` BEFORE any producer can fire) and `take_receiver()`
/// (called by `run_interactive` to own the rx for its select
/// loop). Mutex<Option<_>> because there's no atomic
/// take-Option-by-move primitive on stable.
static RX_HOLDER: = new;
/// Create the channel and stash both sender + receiver. Call this
/// EARLY in `main()` (review #1), before any producer (MCP
/// stderr forwarder, plugin worker) can fire. The UI loop calls
/// `take_receiver()` to claim the receiver when it spins up.
///
/// Re-installing replaces the previous channel. A producer
/// holding a clone of the OLD sender will see `try_send` fail
/// (closed channel) on its next call and the slot is cleared
/// (review #2). New producers get the live sender via `sender()`.
/// Claim the receiver. Called once by the UI loop at startup.
/// Returns `None` if `install()` was never called OR a previous
/// caller already took the receiver — both edge cases mean "no UI
/// notification path available", and the caller should
/// `std::future::pending()`-await as a no-op arm.
/// Get a clone of the live sender. Returns `None` if `install()`
/// hasn't been called yet (very early startup, CLI-only paths,
/// tests). Producers should `.ok()`-style the failure.
/// Send an MCP log line through the notification channel.
/// Convenience wrapper for the stderr forwarder. Uses `try_send`
/// — drops the line if the queue is full (review #4), and detects
/// orphaned senders to clear the slot (review #2) so producers
/// don't keep pumping into a dead channel after a UI restart.
///
/// Receiver-side sanitization (review #7) happens in the UI event
/// loop so EVERY notification gets stripped of control bytes
/// regardless of how careful the producer was.
/// Generic send helper — used by future Info/Warn/Error producers
/// so they share the orphan-detection + bounded-drop semantics.