feat(hooks): label each attention event in the toast

The toast body showed Claude's own message when present and the raw
notification_type otherwise, so permission_prompt and idle_prompt reached
the user as snake_case. Map every event the matcher covers to a readable
label, and keep Claude's message as a suffix when it adds detail.
This commit is contained in:
Bastien Chanot
2026-09-03 02:24:51 +02:00
parent 2ed51573f7
commit 679c2cda7b
+20 -12
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Notification + Stop hook — signal the user through the terminal when # Notification + Stop hook — signal the user through the terminal when
# Claude needs input (permission prompt, question, idle wait) or has # Claude needs input (permission, question, idle wait) or has finished
# finished responding. # responding. Each case gets its own readable label so the toast says
# which one fired.
# #
# Runs on the remote (Linux); the only channel that crosses SSH into the # Runs on the remote (Linux); the only channel that crosses SSH into the
# VS Code client is the terminal stream. Hooks have no controlling TTY, # VS Code client is the terminal stream. Hooks have no controlling TTY,
@@ -12,27 +13,34 @@
# volume for Code in the Windows volume mixer (BLK-020). # volume for Code in the Windows volume mixer (BLK-020).
# - OSC 777 notify -> Windows toast via the client-side extension # - OSC 777 notify -> Windows toast via the client-side extension
# "Terminal Notification" (wenbopan.vscode-terminal-osc-notifier). # "Terminal Notification" (wenbopan.vscode-terminal-osc-notifier).
# The extension only instruments terminals created AFTER it is # A terminal can be deaf to OSC while the bell still rings; test it
# active: install it first, then start or re-attach the session. # before attaching a session to it (LRN-148).
# Both are invisible no-ops in terminals that ignore them. # Both are invisible no-ops in terminals that ignore them.
set -u set -u
payload=$(cat 2>/dev/null) payload=$(cat 2>/dev/null)
read_field() { read_field() {
printf '%s' "$payload" | jq -r "$1 // empty" 2>/dev/null \ printf '%s' "$payload" | jq -r "$1 // empty" 2>/dev/null \
| tr -d '\000-\037' | cut -c1-200 | tr -d '\000-\037' | cut -c1-160
} }
msg=$(read_field '.message // .notification_type') event=$(read_field '.notification_type')
if [ -z "$msg" ]; then [ -n "$event" ] || event=$(read_field '.hook_event_name')
case "$(read_field '.hook_event_name')" in
Stop) msg="Claude has finished responding" ;; case "$event" in
*) msg="Claude Code needs your input" ;; Stop) label="Finished responding" ;;
permission_prompt) label="Needs your permission" ;;
agent_needs_input) label="Asks you a question" ;;
idle_prompt) label="Waiting for you" ;;
elicitation_dialog|elicitation_url_dialog) label="Needs your input" ;;
*) label="Needs your attention" ;;
esac esac
fi
detail=$(read_field '.message')
[ -z "$detail" ] || label="${label}: ${detail}"
bell=$(printf '\a') bell=$(printf '\a')
esc=$(printf '\033') esc=$(printf '\033')
seq="${bell}${bell}${esc}]777;notify;Claude Code;${msg}${esc}\\" seq="${bell}${bell}${esc}]777;notify;Claude Code;${label}${esc}\\"
jq -cn --arg seq "$seq" '{suppressOutput: true, terminalSequence: $seq}' jq -cn --arg seq "$seq" '{suppressOutput: true, terminalSequence: $seq}'
exit 0 exit 0