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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! Native toast surfacing — the fallback for when a `toast: true`
//! notification arrives but no Client App is subscribed to receive the
//! live push (KLP Phase E, #102).
//!
//! (The module is still named `emergency_notify` for history — this path
//! was originally emergency-only. It now fires for any notification with
//! `toast: true`, decoupled from `priority`; the gate lives in
//! [`crate::klp::notify_bus`].)
//!
//! The notification bus is at-most-once (SPEC §2.12.7): a notification
//! published while no Client App is connected is normally dropped and
//! recovered later via `notifications.list`. That's fine for an in-app-only
//! notification, but a `toast: true` one — whose whole point is to grab
//! attention *now* — must not wait for the user to happen to open the app,
//! especially since the Client App does not autostart on logon, so "no
//! client connected" is a common state.
//!
//! So when [`crate::klp::notify_bus`] sees a `toast: true` notification with
//! zero broadcast receivers, it calls [`surface_toast_notification`], which
//! **launches the Client App** in the active console session (via
//! [`crate::process_as_user::launch_detached_in_user_session`], the same
//! WTS token path the agent already uses for `run_as: user`), passing the
//! notification id. The launched client starts **hidden** (no window) and
//! shows only a native **toast** — so it never "bursts" over whatever the
//! user is doing (e.g. a meeting). Clicking the toast is what brings the
//! window forward, focused on the notification panel.
//!
//! We deliberately do NOT pop a `WTSSendMessageW` message box here: a
//! blocking system dialog is exactly the screen-takeover the toast design
//! avoids.
//!
//! Best-effort: the launch is fire-and-forget on a blocking thread so the
//! bus loop never stalls; a missing client install or a spawn failure is
//! a logged no-op, never propagated.
use PathBuf;
use ;
use ;
use OnTrigger;
use ;
/// Path of the installed Client App under `%ProgramFiles%` — set by the
/// `install-kanade-client` job (`<ProgramFiles>\Kanade\kanade-client.exe`).
const CLIENT_EXE_REL: &str = r"Kanade\kanade-client.exe";
/// CLI flag the launched client reads to show the notification as a toast
/// (hidden window) instead of its normal visible startup. Kept in sync
/// with the client's arg parser (`kanade-client`'s `app.rs`).
const SHOW_NOTIFICATION_ARG: &str = "--show-notification";
/// CLI flag telling the client to **re-toast every still-unread, unexpired
/// `toast: true` notification** (hidden window), bypassing its in-app
/// duplicate-suppression — used to re-pop ones the user couldn't see when
/// they arrived (sent while signed out, or delivered to the Action Center
/// while the screen was locked). Whether the client is launched fresh or
/// this is forwarded to an already-running instance, it re-pops. Kept in
/// sync with `app.rs`.
const RESURFACE_ARG: &str = "--resurface";
/// Resolve the installed Client App path, or `None` when it isn't
/// installed (so the fallback is a clean no-op rather than spawning a
/// missing exe).
/// Launch throttle window: launching the heavy Tauri/WebView client is
/// expensive, so a burst of notifications (or a logon racing a fallback) must
/// not spawn it over and over. At most one launch per window.
const COOLDOWN_SECS: u64 = 10;
static LAST_LAUNCH_SECS: AtomicU64 = new;
/// Set when a `toast: true` notification was processed while the user was
/// **not present** — signed out (no console session) or signed in but
/// **locked** — so the toast either couldn't be shown or went silently to the
/// Action Center. Re-popped the next time the user becomes present (logon or
/// unlock; see [`on_session_event`]). In-memory only: an agent restart loses
/// it, but the notification is still in the 90-day NOTIFICATIONS stream and
/// recovers when the user next opens the Client App by hand (#647).
static PENDING_RESURFACE: AtomicBool = new;
/// Whether the console session is currently **locked**. Tracked from the SCM
/// `SessionLock` / `SessionUnlock` events ([`on_session_event`]). Defaults to
/// unlocked: if the agent (re)starts while locked we won't know until the next
/// event, an accepted edge.
static LOCKED: AtomicBool = new;
/// True when an interactive user is attached to the physical console.
/// `WTSGetActiveConsoleSessionId` returns `0xFFFFFFFF` when none is (no
/// signed-in user) — in which case there's nobody to toast at.
/// True when a user can actually *see* a toast right now: signed in AND not
/// locked. When false, a toast notification must be re-popped on the next
/// presence.
/// Launch the installed Client App in the active user session, fire-and-forget
/// on a **detached OS thread** — so it works whether the caller is on the
/// tokio runtime (the notify bus) or the SCM control thread
/// ([`on_session_event`], which runs outside any runtime). `args` pass through
/// to the client; throttled to one launch per [`COOLDOWN_SECS`]. A missing
/// install or spawn failure is a logged no-op, never panics, never propagates.
/// Surface a `toast: true` notification by launching the Client App in the
/// user's session to toast it (the **no-subscribed-client fallback**).
/// Fire-and-forget; never panics, never propagates.
///
/// Presence-aware (#647):
/// - **Signed out** (no console session) → nobody to toast at, so flag for
/// re-pop on the next logon instead of launching into the void.
/// - **Locked** → still launch (the toast lands in the Action Center as a
/// backstop) *and* flag for re-pop on unlock, since a toast that arrives
/// while locked is never actively shown.
/// - **Present** → launch and the toast shows immediately.
/// Note that a `toast: true` notification was **live-pushed to an
/// already-connected client** (so [`surface_toast_notification`]'s fallback
/// didn't run). If the user isn't present — i.e. locked — the client's toast
/// went silently to the Action Center, so flag it for re-pop on the next
/// presence. Called from the notify bus for every toast that reaches a
/// subscriber.
/// React to an OS session event (#647). On **logon** or **unlock** — i.e. the
/// user becoming present — if a toast notification was deferred while they were
/// away, launch the client with `--resurface` so it re-pops every still-unread,
/// unexpired `toast: true` notification (`toast: false` ones stay passive by
/// design). Also tracks the lock state. A no-op when nothing was deferred.
/// If a toast notification was deferred while the user was away, re-pop it now
/// that they're present.