Skip to content

Commit 9284957

Browse files
Chebaleomkaromkar-aiplanet
authored andcommitted
Add Windows notification support via native toast fallback
On Windows, /dev/tty fails inside Claude Code's hook runner because stdio is captured. OSC 777 escape sequences never reach Warp's terminal emulator, so notifications silently fail. This adds a Windows-native fallback using PowerShell's Windows.UI.Notifications API: - win-toast.ps1: Sends native Windows toasts branded as Warp (registers dev.warp.Warp AppUserModelId with Warp's icon) - win-notify.sh: Deduplication (mkdir lock, 8s window) + event-type routing with project context in notification title - Patched on-stop.sh, on-notification.sh, on-permission-request.sh to call win-notify.sh after warp-notify.sh - Patched warp-notify.sh to try /dev/tty first, fall back gracefully Zero dependencies — uses built-in Windows 10/11 APIs. macOS/Linux behavior is unchanged. Fixes #48
1 parent b8ad3cc commit 9284957

6 files changed

Lines changed: 161 additions & 2 deletions

File tree

plugins/warp/scripts/on-notification.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ BODY=$(build_payload "$INPUT" "$NOTIF_TYPE" \
2525
--arg summary "$MSG")
2626

2727
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
28+
29+
# Windows fallback: native toast notification (OSC 777 fails on Windows)
30+
[ -f "$SCRIPT_DIR/win-notify.sh" ] && "$SCRIPT_DIR/win-notify.sh" "$NOTIF_TYPE" "$INPUT"

plugins/warp/scripts/on-permission-request.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ BODY=$(build_payload "$INPUT" "permission_request" \
3737
--argjson tool_input "$TOOL_INPUT")
3838

3939
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
40+
41+
# Windows fallback: native toast notification (OSC 777 fails on Windows)
42+
[ -f "$SCRIPT_DIR/win-notify.sh" ] && "$SCRIPT_DIR/win-notify.sh" "permission_request" "$INPUT"

plugins/warp/scripts/on-stop.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,6 @@ BODY=$(build_payload "$INPUT" "stop" \
7171
--arg transcript_path "$TRANSCRIPT_PATH")
7272

7373
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
74+
75+
# Windows fallback: native toast notification (OSC 777 fails on Windows)
76+
[ -f "$SCRIPT_DIR/win-notify.sh" ] && "$SCRIPT_DIR/win-notify.sh" "stop" "$INPUT"

plugins/warp/scripts/warp-notify.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,11 @@ TITLE="${1:-Notification}"
1717
BODY="${2:-}"
1818

1919
# OSC 777 format: \033]777;notify;<title>;<body>\007
20-
# Write directly to /dev/tty to ensure it reaches the terminal
21-
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null || true
20+
# Try /dev/tty first (macOS/Linux). On Windows, /dev/tty fails inside
21+
# Claude Code's hook runner because stdio is captured. Fall back to stderr.
22+
if printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null; then
23+
exit 0
24+
fi
25+
26+
# Last resort: stderr (may not reach terminal in sandboxed hook contexts)
27+
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" >&2 2>/dev/null || true

plugins/warp/scripts/win-notify.sh

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/bin/bash
2+
# Windows-only notification sender with deduplication
3+
# Usage: win-notify.sh <event_type> <json_input>
4+
#
5+
# Only one notification per 8 seconds to prevent duplicates from
6+
# multiple hooks firing on the same Claude Code event.
7+
8+
# Only run on Windows
9+
[ -z "$WINDIR" ] && exit 0
10+
command -v powershell &>/dev/null || exit 0
11+
12+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13+
14+
EVENT_TYPE="${1:-unknown}"
15+
INPUT="${2:-{}}"
16+
17+
# === Deduplication ===
18+
LOCK_DIR="/tmp/warp-notify-lock"
19+
if mkdir "$LOCK_DIR" 2>/dev/null; then
20+
# We got the lock — we're the first hook to fire
21+
# Clean up lock after 8 seconds (background)
22+
(sleep 8 && rmdir "$LOCK_DIR" 2>/dev/null) &
23+
else
24+
# Another hook already fired recently — skip
25+
exit 0
26+
fi
27+
28+
# === Extract context ===
29+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
30+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
31+
PROJECT=""
32+
if [ -n "$CWD" ]; then
33+
PROJECT=$(basename "$CWD")
34+
fi
35+
36+
# === Build notification title and body based on event type ===
37+
case "$EVENT_TYPE" in
38+
stop)
39+
NOTIF_TITLE="✅ Task Completed"
40+
# Try to get the response summary
41+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
42+
RESPONSE=""
43+
QUERY=""
44+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
45+
RESPONSE=$(jq -rs '
46+
[.[] | select(.type == "assistant" and .message.content)] | last |
47+
[.message.content[] | select(.type == "text") | .text] | join(" ")
48+
' "$TRANSCRIPT_PATH" 2>/dev/null)
49+
QUERY=$(jq -rs '
50+
[
51+
.[] | select(.type == "user") |
52+
if .message.content | type == "string" then .
53+
elif [.message.content[] | select(.type == "text")] | length > 0 then .
54+
else empty end
55+
] | last |
56+
if .message.content | type == "array"
57+
then [.message.content[] | select(.type == "text") | .text] | join(" ")
58+
else .message.content // empty end
59+
' "$TRANSCRIPT_PATH" 2>/dev/null)
60+
fi
61+
if [ -n "$RESPONSE" ]; then
62+
NOTIF_BODY="${RESPONSE:0:200}"
63+
elif [ -n "$QUERY" ]; then
64+
NOTIF_BODY="Done: ${QUERY:0:200}"
65+
else
66+
NOTIF_BODY="Claude finished the task"
67+
fi
68+
;;
69+
idle_prompt)
70+
NOTIF_TITLE="⏳ Input Needed"
71+
MSG=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
72+
NOTIF_BODY="${MSG:-Claude is waiting for your input}"
73+
;;
74+
permission_request)
75+
NOTIF_TITLE="🔐 Permission Required"
76+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "a tool"' 2>/dev/null)
77+
NOTIF_BODY="Claude wants to run: $TOOL_NAME"
78+
;;
79+
session_start)
80+
# Don't notify on session start — not useful
81+
rmdir "$LOCK_DIR" 2>/dev/null
82+
exit 0
83+
;;
84+
*)
85+
NOTIF_TITLE="Claude Code"
86+
NOTIF_BODY="Needs your attention"
87+
;;
88+
esac
89+
90+
# === Add project context ===
91+
if [ -n "$PROJECT" ]; then
92+
NOTIF_TITLE="$NOTIF_TITLE$PROJECT"
93+
fi
94+
95+
# === Fire Windows notification ===
96+
powershell -ExecutionPolicy Bypass -NoProfile -File "$SCRIPT_DIR/win-toast.ps1" \
97+
-Title "$NOTIF_TITLE" -Body "$NOTIF_BODY" &>/dev/null &

plugins/warp/scripts/win-toast.ps1

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Windows native toast notification branded as Warp
2+
# Usage: powershell -ExecutionPolicy Bypass -File win-toast.ps1 -Title "title" -Body "body"
3+
param(
4+
[string]$Title = "Claude Code",
5+
[string]$Body = "Task complete"
6+
)
7+
8+
# --- Register Warp as a notification source (one-time, no admin needed) ---
9+
$appId = "dev.warp.Warp"
10+
$regPath = "HKCU:\SOFTWARE\Classes\AppUserModelId\$appId"
11+
$iconPath = "$env:LOCALAPPDATA\Programs\Warp\icon.ico"
12+
13+
if (-not (Test-Path $regPath)) {
14+
New-Item -Path $regPath -Force | Out-Null
15+
}
16+
New-ItemProperty -Path $regPath -Name "DisplayName" -Value "Warp" -PropertyType String -Force | Out-Null
17+
if (Test-Path $iconPath) {
18+
New-ItemProperty -Path $regPath -Name "IconUri" -Value $iconPath -PropertyType ExpandString -Force | Out-Null
19+
}
20+
21+
# --- Load Windows Runtime types ---
22+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
23+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
24+
25+
# --- Build toast XML ---
26+
$logoAttr = ""
27+
if (Test-Path $iconPath) {
28+
$logoAttr = "<image placement=`"appLogoOverride`" src=`"$iconPath`" hint-crop=`"circle`"/>"
29+
}
30+
31+
$toastXml = @"
32+
<toast>
33+
<visual>
34+
<binding template="ToastGeneric">
35+
$logoAttr
36+
<text>$Title</text>
37+
<text>$Body</text>
38+
</binding>
39+
</visual>
40+
</toast>
41+
"@
42+
43+
# --- Show notification ---
44+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
45+
$xml.LoadXml($toastXml)
46+
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
47+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)

0 commit comments

Comments
 (0)