From a57c3c5dd5158c5be2141c9a4a4e8bbdb38f95d6 Mon Sep 17 00:00:00 2001 From: boss Date: Mon, 30 Mar 2026 13:46:48 +0800 Subject: [PATCH] fix(hooks): add WSL desktop notification support via PowerShell + BurntToast Adds WSL (Windows Subsystem for Linux) desktop notification support to the existing desktop-notify hook. The hook now detects WSL, finds available PowerShell (7 or Windows PowerShell), checks for BurntToast module, and sends Windows toast notifications. New functions: - isWSL(): detects WSL environment - findPowerShell(): finds PowerShell 7 or Windows PowerShell on WSL - isBurntToastAvailable(): checks if BurntToast module is installed - notifyWindows(): sends Windows toast notification via BurntToast If BurntToast is not installed, logs helpful tip for installation. Falls back silently on non-WSL/non-macOS platforms. --- scripts/hooks/desktop-notify.js | 99 ++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/scripts/hooks/desktop-notify.js b/scripts/hooks/desktop-notify.js index 8d844e03..75375500 100644 --- a/scripts/hooks/desktop-notify.js +++ b/scripts/hooks/desktop-notify.js @@ -3,9 +3,11 @@ * Desktop Notification Hook (Stop) * * Sends a native desktop notification with the task summary when Claude - * finishes responding. Currently supports macOS (osascript); other - * platforms exit silently. Windows (PowerShell) and Linux (notify-send) - * support is planned. + * finishes responding. Supports: + * - macOS: osascript (native) + * - WSL: PowerShell 7 or Windows PowerShell + BurntToast module + * + * On WSL, if BurntToast is not installed, logs a tip for installation. * * Hook ID : stop:desktop-notify * Profiles: standard, strict @@ -19,6 +21,60 @@ const { isMacOS, log } = require('../lib/utils'); const TITLE = 'Claude Code'; const MAX_BODY_LENGTH = 100; +/** + * Check if running on WSL (Windows Subsystem for Linux). + */ +function isWSL() { + if (process.platform !== 'linux') return false; + try { + return require('fs').readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); + } catch { + return false; + } +} + +/** + * Find available PowerShell executable on WSL. + * Checks PowerShell 7 first, then falls back to Windows PowerShell. + * Returns { path, version } or null if none available. + */ +function findPowerShell() { + if (!isWSL()) return null; + + const candidates = [ + '/mnt/c/Program Files/PowerShell/7/pwsh.exe', // PowerShell 7 + '/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe', // Windows PowerShell + ]; + + for (const path of candidates) { + try { + const result = spawnSync(path, ['-Command', '$PSVersionTable.PSVersion.Major'], + { stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }); + if (result.status === 0) { + const version = parseInt(result.stdout.toString().trim(), 10); + return { path, version }; + } + } catch { + // continue + } + } + return null; +} + +/** + * Check if BurntToast module is available on the given PowerShell path. + */ +function isBurntToastAvailable(pwshPath) { + try { + const result = spawnSync(pwshPath, + ['-Command', 'Import-Module BurntToast -ErrorAction Stop; $true'], + { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }); + return result.status === 0; + } catch { + return false; + } +} + /** * Extract a short summary from the last assistant message. * Takes the first non-empty line and truncates to MAX_BODY_LENGTH chars. @@ -53,20 +109,45 @@ function notifyMacOS(title, body) { } } -// TODO: future platform support -// function notifyWindows(title, body) { ... } -// function notifyLinux(title, body) { ... } +/** + * Send a Windows Toast notification via PowerShell BurntToast. + * Used when running under WSL to show notification on Windows desktop. + */ +function notifyWindows(pwshPath, title, body) { + const safeBody = body.replace(/'/g, "''"); + const safeTitle = title.replace(/'/g, "''"); + const command = `Import-Module BurntToast; New-BurntToastNotification -Text '${safeTitle}', '${safeBody}'`; + const result = spawnSync(pwshPath, ['-Command', command], { stdio: 'ignore', timeout: 5000 }); + if (result.error || result.status !== 0) { + log(`[DesktopNotify] BurntToast failed (exit ${result.status}): ${result.error ? result.error.message : result.stderr?.toString()}`); + } +} /** * Fast-path entry point for run-with-flags.js (avoids extra process spawn). */ function run(raw) { try { - if (!isMacOS) return raw; - const input = raw.trim() ? JSON.parse(raw) : {}; const summary = extractSummary(input.last_assistant_message); - notifyMacOS(TITLE, summary); + + if (isMacOS) { + notifyMacOS(TITLE, summary); + } else if (isWSL()) { + // WSL: try PowerShell 7 first, then Windows PowerShell + const ps = findPowerShell(); + if (ps && isBurntToastAvailable(ps.path)) { + notifyWindows(ps.path, TITLE, summary); + } else if (ps) { + // PowerShell exists but no BurntToast module + log('[DesktopNotify] Tip: Install BurntToast module to enable notifications:'); + log(`[DesktopNotify] ${ps.path} -Command "Install-Module -Name BurntToast -Scope CurrentUser"`); + } else { + // No PowerShell found + log('[DesktopNotify] Tip: Install BurntToast in PowerShell for notifications:'); + log('[DesktopNotify] https://github.com/microsoft/BurntToast'); + } + } } catch (err) { log(`[DesktopNotify] Error: ${err.message}`); }