@erica-s/ai-agent-notify 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ # Tab 颜色 watcher:先附着到目标 shell 的 console,保证 hook 场景也能命中目标 tab;
2
+ # 用户回到目标窗口后,再通过标准流和附着 console 双通道写 reset OSC 恢复默认颜色。
3
+ # 作为独立进程运行。
4
+ param(
5
+ [Parameter(Mandatory)][int]$TargetPid,
6
+ [string]$HookEvent = '',
7
+ [long]$TerminalHwnd = 0
8
+ )
9
+
10
+ $ErrorActionPreference = 'Stop'
11
+
12
+ # --- 日志 ---
13
+ function Write-Log($msg) {
14
+ $line = "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.fffZ')] [watcher pid=$PID] $msg"
15
+ if ($env:TOAST_NOTIFY_LOG_FILE) {
16
+ try { [System.IO.File]::AppendAllText($env:TOAST_NOTIFY_LOG_FILE, "$line`n") } catch {}
17
+ }
18
+ }
19
+
20
+ Write-Log "started TargetPid=$TargetPid HookEvent=$HookEvent TerminalHwnd=$TerminalHwnd"
21
+
22
+ # --- 颜色映射 ---
23
+ $colorMap = @{
24
+ 'Stop' = 'rgb:33/cc/33'
25
+ 'PermissionRequest' = 'rgb:ff/99/00'
26
+ }
27
+ $color = if ($colorMap.ContainsKey($HookEvent)) { $colorMap[$HookEvent] } else { 'rgb:33/99/ff' }
28
+
29
+ $ESC = [char]0x1B
30
+ $ST = "$ESC\"
31
+
32
+ # --- P/Invoke 定义 ---
33
+ Add-Type -TypeDefinition @'
34
+ using System;
35
+ using System.Runtime.InteropServices;
36
+ using System.Threading;
37
+
38
+ [StructLayout(LayoutKind.Sequential)]
39
+ public struct LASTINPUTINFO {
40
+ public uint cbSize;
41
+ public uint dwTime;
42
+ }
43
+
44
+ public class TabWatcher {
45
+ [DllImport("kernel32.dll", SetLastError=true)]
46
+ public static extern bool FreeConsole();
47
+
48
+ [DllImport("kernel32.dll", SetLastError=true)]
49
+ public static extern bool AttachConsole(uint dwProcessId);
50
+
51
+ [DllImport("kernel32.dll", SetLastError=true)]
52
+ public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
53
+
54
+ [DllImport("kernel32.dll", SetLastError=true)]
55
+ public static extern bool CloseHandle(IntPtr hObject);
56
+
57
+ [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
58
+ public static extern IntPtr CreateFileW(
59
+ string lpFileName, uint dwDesiredAccess, uint dwShareMode,
60
+ IntPtr lpSecurityAttributes, uint dwCreationDisposition,
61
+ uint dwFlagsAndAttributes, IntPtr hTemplateFile);
62
+
63
+ [DllImport("kernel32.dll", SetLastError=true)]
64
+ public static extern bool WriteFile(
65
+ IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite,
66
+ out uint lpNumberOfBytesWritten, IntPtr lpOverlapped);
67
+
68
+ [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
69
+ public static extern bool WriteConsoleW(
70
+ IntPtr hConsoleOutput, string lpBuffer, uint nNumberOfCharsToWrite,
71
+ out uint lpNumberOfCharsWritten, IntPtr lpReserved);
72
+
73
+ [DllImport("kernel32.dll", SetLastError=true)]
74
+ public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
75
+
76
+ [DllImport("kernel32.dll", SetLastError=true)]
77
+ public static extern uint WaitForMultipleObjects(uint nCount, IntPtr[] lpHandles, bool bWaitAll, uint dwMilliseconds);
78
+
79
+ [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
80
+ public static extern IntPtr CreateMutexW(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);
81
+
82
+ [DllImport("kernel32.dll", SetLastError=true)]
83
+ public static extern bool ReleaseMutex(IntPtr hMutex);
84
+
85
+ [DllImport("user32.dll", SetLastError=true)]
86
+ public static extern IntPtr GetForegroundWindow();
87
+
88
+ [DllImport("user32.dll", SetLastError=true)]
89
+ public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
90
+
91
+ [DllImport("kernel32.dll", SetLastError=true)]
92
+ public static extern bool GetNumberOfConsoleInputEvents(IntPtr hConsoleInput, out uint lpcNumberOfEvents);
93
+
94
+ public const uint SYNCHRONIZE = 0x00100000;
95
+ public const uint GENERIC_READ = 0x80000000;
96
+ public const uint GENERIC_WRITE = 0x40000000;
97
+ public const uint FILE_SHARE_READ = 0x00000001;
98
+ public const uint FILE_SHARE_WRITE = 0x00000002;
99
+ public const uint OPEN_EXISTING = 3;
100
+ public const uint WAIT_OBJECT_0 = 0;
101
+ public const uint WAIT_ABANDONED_0 = 0x00000080;
102
+ public const uint WAIT_TIMEOUT = 0x00000102;
103
+ public const uint WAIT_FAILED = 0xFFFFFFFF;
104
+ public const int ERROR_ALREADY_EXISTS = 183;
105
+ public static void SleepMs(int milliseconds) {
106
+ Thread.Sleep(milliseconds);
107
+ }
108
+
109
+ public static uint GetLastInputTick() {
110
+ LASTINPUTINFO info = new LASTINPUTINFO();
111
+ info.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
112
+ if (!GetLastInputInfo(ref info)) {
113
+ return 0;
114
+ }
115
+ return info.dwTime;
116
+ }
117
+ }
118
+ '@
119
+
120
+ function Write-OscToInheritedStreams([string]$seq, [string]$label) {
121
+ $data = [System.Text.Encoding]::ASCII.GetBytes($seq)
122
+ $stdoutOk = $false
123
+ $stderrOk = $false
124
+
125
+ try {
126
+ $stdout = [Console]::OpenStandardOutput()
127
+ $stdout.Write($data, 0, $data.Length)
128
+ $stdout.Flush()
129
+ $stdoutOk = $true
130
+ } catch {
131
+ Write-Log "$label via inherited stdout failed: $_"
132
+ }
133
+
134
+ try {
135
+ $stderr = [Console]::OpenStandardError()
136
+ $stderr.Write($data, 0, $data.Length)
137
+ $stderr.Flush()
138
+ $stderrOk = $true
139
+ } catch {
140
+ Write-Log "$label via inherited stderr failed: $_"
141
+ }
142
+
143
+ if ($stdoutOk -or $stderrOk) {
144
+ Write-Log "$label via inherited streams stdout=$stdoutOk stderr=$stderrOk"
145
+ return $true
146
+ }
147
+
148
+ Write-Log "$label failed on both inherited streams"
149
+ return $false
150
+ }
151
+
152
+ function Attach-TargetConsole {
153
+ [TabWatcher]::FreeConsole() | Out-Null
154
+ $attached = [TabWatcher]::AttachConsole([uint32]$TargetPid)
155
+ if (-not $attached) {
156
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
157
+ Write-Log "AttachConsole failed err=$err"
158
+ return [IntPtr]::Zero
159
+ }
160
+
161
+ Write-Log "attached to console of pid=$TargetPid"
162
+ $hIn = [TabWatcher]::CreateFileW(
163
+ "CONIN$",
164
+ [TabWatcher]::GENERIC_READ -bor [TabWatcher]::GENERIC_WRITE,
165
+ [TabWatcher]::FILE_SHARE_READ -bor [TabWatcher]::FILE_SHARE_WRITE,
166
+ [IntPtr]::Zero,
167
+ [TabWatcher]::OPEN_EXISTING,
168
+ 0,
169
+ [IntPtr]::Zero
170
+ )
171
+ $invalidHandle = [IntPtr](-1)
172
+ if ($hIn -eq $invalidHandle) {
173
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
174
+ Write-Log "CreateFileW CONIN$ failed err=$err"
175
+ $hIn = [IntPtr]::Zero
176
+ } else {
177
+ Write-Log "opened CONIN$ handle=$hIn"
178
+ }
179
+
180
+ $hOut = [TabWatcher]::CreateFileW(
181
+ "CONOUT$",
182
+ [TabWatcher]::GENERIC_READ -bor [TabWatcher]::GENERIC_WRITE,
183
+ [TabWatcher]::FILE_SHARE_READ -bor [TabWatcher]::FILE_SHARE_WRITE,
184
+ [IntPtr]::Zero,
185
+ [TabWatcher]::OPEN_EXISTING,
186
+ 0,
187
+ [IntPtr]::Zero
188
+ )
189
+ if ($hOut -eq $invalidHandle) {
190
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
191
+ Write-Log "CreateFileW CONOUT$ failed err=$err"
192
+ $hOut = [IntPtr]::Zero
193
+ } else {
194
+ Write-Log "opened CONOUT$ handle=$hOut"
195
+ }
196
+
197
+ return @{
198
+ InputHandle = $hIn
199
+ OutputHandle = $hOut
200
+ }
201
+ }
202
+
203
+ function Write-OscToAttachedConsole([IntPtr]$hOut, [string]$seq, [string]$label) {
204
+ if ($hOut -eq [IntPtr]::Zero) {
205
+ Write-Log "$label via attached console skipped: no handle"
206
+ return $false
207
+ }
208
+
209
+ $consoleOk = $false
210
+ try {
211
+ $charsWritten = 0
212
+ $consoleOk = [TabWatcher]::WriteConsoleW($hOut, $seq, [uint32]$seq.Length, [ref]$charsWritten, [IntPtr]::Zero)
213
+ if ($consoleOk -and $charsWritten -eq $seq.Length) {
214
+ Write-Log "$label via attached console WriteConsoleW chars=$charsWritten"
215
+ return $true
216
+ }
217
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
218
+ Write-Log "$label via attached console WriteConsoleW failed err=$err chars=$charsWritten"
219
+ } catch {
220
+ Write-Log "$label via attached console WriteConsoleW exception: $_"
221
+ }
222
+
223
+ try {
224
+ $data = [System.Text.Encoding]::ASCII.GetBytes($seq)
225
+ $bytesWritten = 0
226
+ $fileOk = [TabWatcher]::WriteFile($hOut, $data, [uint32]$data.Length, [ref]$bytesWritten, [IntPtr]::Zero)
227
+ if ($fileOk -and $bytesWritten -eq $data.Length) {
228
+ Write-Log "$label via attached console WriteFile bytes=$bytesWritten"
229
+ return $true
230
+ }
231
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
232
+ Write-Log "$label via attached console WriteFile failed err=$err bytes=$bytesWritten"
233
+ } catch {
234
+ Write-Log "$label via attached console WriteFile exception: $_"
235
+ }
236
+
237
+ return $false
238
+ }
239
+
240
+ function Get-ConsoleInputEventCount([IntPtr]$hIn) {
241
+ if ($hIn -eq [IntPtr]::Zero) {
242
+ return $null
243
+ }
244
+
245
+ $count = 0
246
+ $ok = [TabWatcher]::GetNumberOfConsoleInputEvents($hIn, [ref]$count)
247
+ if (-not $ok) {
248
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
249
+ Write-Log "GetNumberOfConsoleInputEvents failed err=$err"
250
+ return $null
251
+ }
252
+ return [uint32]$count
253
+ }
254
+
255
+ # --- Named Mutex 防重复 ---
256
+ $mutexName = "Global\claude-notify-tab-$TargetPid"
257
+ $hMutex = [TabWatcher]::CreateMutexW([IntPtr]::Zero, $true, $mutexName)
258
+ if ($hMutex -eq [IntPtr]::Zero) {
259
+ Write-Log "CreateMutex failed, exiting"
260
+ exit 1
261
+ }
262
+ $lastErr = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
263
+ if ($lastErr -eq [TabWatcher]::ERROR_ALREADY_EXISTS) {
264
+ Write-Log "mutex already exists, waiting for old watcher to release"
265
+ $waitResult = [TabWatcher]::WaitForSingleObject($hMutex, 5000)
266
+ if ($waitResult -eq [TabWatcher]::WAIT_FAILED) {
267
+ Write-Log "failed to acquire mutex, exiting"
268
+ [TabWatcher]::CloseHandle($hMutex)
269
+ exit 1
270
+ }
271
+ Write-Log "acquired mutex from old watcher"
272
+ }
273
+
274
+ $exitCode = 0
275
+ $hIn = [IntPtr]::Zero
276
+ $hOut = [IntPtr]::Zero
277
+ $hProcess = [IntPtr]::Zero
278
+ try {
279
+ $consoleHandles = Attach-TargetConsole
280
+ if ($consoleHandles) {
281
+ $hIn = $consoleHandles.InputHandle
282
+ $hOut = $consoleHandles.OutputHandle
283
+ }
284
+ $setColor = "$ESC]4;264;$color$ST"
285
+ $setViaAttached = Write-OscToAttachedConsole $hOut $setColor "set tab color=$color"
286
+ $setViaStreams = Write-OscToInheritedStreams $setColor "set tab color=$color"
287
+ Write-Log "set color summary attached=$setViaAttached streams=$setViaStreams"
288
+
289
+ $hProcess = [TabWatcher]::OpenProcess([TabWatcher]::SYNCHRONIZE, $false, [uint32]$TargetPid)
290
+ if ($hProcess -eq [IntPtr]::Zero) {
291
+ $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
292
+ throw "OpenProcess failed err=$err"
293
+ }
294
+
295
+ $baselineForeground = [TabWatcher]::GetForegroundWindow()
296
+ $baselineInputTick = [TabWatcher]::GetLastInputTick()
297
+ $baselineConsoleInputCount = Get-ConsoleInputEventCount $hIn
298
+ $consoleInputArmed = ($null -eq $baselineConsoleInputCount) -or ($baselineConsoleInputCount -eq 0)
299
+ $sawTargetConsoleInput = $false
300
+ $lastDrainCount = $baselineConsoleInputCount
301
+ Write-Log "waiting for target console input + foreground return baselineForeground=$baselineForeground baselineInputTick=$baselineInputTick baselineConsoleInputCount=$baselineConsoleInputCount armed=$consoleInputArmed"
302
+ if ($TerminalHwnd -le 0) {
303
+ Write-Log "no terminal hwnd provided; reset will only stop when target process exits"
304
+ }
305
+
306
+ $waitHandles = if ($hIn -ne [IntPtr]::Zero) { @($hProcess, $hIn) } else { @($hProcess) }
307
+
308
+ while ($true) {
309
+ $result = if ($waitHandles.Count -gt 1) {
310
+ [TabWatcher]::WaitForMultipleObjects([uint32]$waitHandles.Count, $waitHandles, $false, 150)
311
+ } else {
312
+ [TabWatcher]::WaitForSingleObject($hProcess, 150)
313
+ }
314
+
315
+ if ($result -eq [TabWatcher]::WAIT_OBJECT_0) {
316
+ Write-Log "target process exited"
317
+ break
318
+ }
319
+
320
+ if ($waitHandles.Count -gt 1 -and $result -eq ([TabWatcher]::WAIT_OBJECT_0 + 1)) {
321
+ if (-not $consoleInputArmed) {
322
+ $currentConsoleInputCount = Get-ConsoleInputEventCount $hIn
323
+ if ($currentConsoleInputCount -eq 0) {
324
+ $consoleInputArmed = $true
325
+ Write-Log "target console input armed after drain"
326
+ } else {
327
+ if ($currentConsoleInputCount -ne $lastDrainCount) {
328
+ Write-Log "target console still draining baselineInputCount=$baselineConsoleInputCount currentInputCount=$currentConsoleInputCount"
329
+ $lastDrainCount = $currentConsoleInputCount
330
+ }
331
+ }
332
+ } else {
333
+ $sawTargetConsoleInput = $true
334
+ Write-Log "target console input signaled"
335
+ }
336
+ }
337
+
338
+ $isExpectedWaitResult =
339
+ ($result -eq [TabWatcher]::WAIT_TIMEOUT) -or
340
+ ($waitHandles.Count -gt 1 -and $result -eq ([TabWatcher]::WAIT_OBJECT_0 + 1))
341
+ if (-not $isExpectedWaitResult) {
342
+ $wmErr = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
343
+ Write-Log "wait returned unexpected result=$result err=$wmErr"
344
+ break
345
+ }
346
+
347
+ if (-not $consoleInputArmed -and $hIn -ne [IntPtr]::Zero) {
348
+ $currentConsoleInputCount = Get-ConsoleInputEventCount $hIn
349
+ if ($currentConsoleInputCount -eq 0) {
350
+ $consoleInputArmed = $true
351
+ Write-Log "target console input armed after timeout drain"
352
+ } elseif ($currentConsoleInputCount -ne $lastDrainCount) {
353
+ Write-Log "target console still draining baselineInputCount=$baselineConsoleInputCount currentInputCount=$currentConsoleInputCount"
354
+ $lastDrainCount = $currentConsoleInputCount
355
+ }
356
+ }
357
+
358
+ $currentForeground = [TabWatcher]::GetForegroundWindow()
359
+ $currentInputTick = [TabWatcher]::GetLastInputTick()
360
+ if ($TerminalHwnd -gt 0 -and $currentForeground -eq [IntPtr]$TerminalHwnd -and $currentInputTick -ne 0 -and $currentInputTick -ne $baselineInputTick -and $sawTargetConsoleInput) {
361
+ Write-Log "foreground returned with target console input foreground=$currentForeground inputTick=$currentInputTick"
362
+ [TabWatcher]::SleepMs(100)
363
+ $resetColor = "$ESC]104;264$ST"
364
+ $resetViaStreams = Write-OscToInheritedStreams $resetColor "reset tab color"
365
+ $resetViaAttached = Write-OscToAttachedConsole $hOut $resetColor "reset tab color"
366
+ Write-Log "reset color summary attached=$resetViaAttached streams=$resetViaStreams"
367
+ break
368
+ }
369
+ }
370
+
371
+ }
372
+ catch {
373
+ Write-Log "error: $_"
374
+ $exitCode = 1
375
+ }
376
+ finally {
377
+ if ($hProcess -ne [IntPtr]::Zero) {
378
+ [TabWatcher]::CloseHandle($hProcess) | Out-Null
379
+ }
380
+ if ($hOut -ne [IntPtr]::Zero) {
381
+ [TabWatcher]::CloseHandle($hOut) | Out-Null
382
+ }
383
+ if ($hIn -ne [IntPtr]::Zero) {
384
+ [TabWatcher]::CloseHandle($hIn) | Out-Null
385
+ }
386
+ [TabWatcher]::ReleaseMutex($hMutex) | Out-Null
387
+ [TabWatcher]::CloseHandle($hMutex) | Out-Null
388
+ Write-Log "exiting code=$exitCode"
389
+ }
390
+
391
+ exit $exitCode