Ending a turn right after spawning a subagent fired the bell and a toast saying the response was finished, while the work continued. The Stop payload carries background_tasks, so skip the signal when it is not empty; the next turn end signals once the work is really done. Interaction requests still signal during background work. A missing field still signals, so an older client loses nothing. Also drop the message suffix when it merely restates the label.
68 lines
2.7 KiB
Bash
Executable File
68 lines
2.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Notification + Stop hook — signal the user through the terminal when
|
|
# Claude needs input (permission, question, idle wait) or has finished
|
|
# 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
|
|
# VS Code client is the terminal stream. Hooks have no controlling TTY,
|
|
# so the sequence goes through the supported `terminalSequence` JSON
|
|
# output field and Claude Code writes it to the terminal:
|
|
# - BEL x2 (double beep) -> sound, needs VS Code setting
|
|
# accessibility.signals.terminalBell { "sound": "on" } AND a non-zero
|
|
# volume for Code in the Windows volume mixer (BLK-020).
|
|
# - OSC 777 notify -> Windows toast via the client-side extension
|
|
# "Terminal Notification" (wenbopan.vscode-terminal-osc-notifier).
|
|
# A terminal can be deaf to OSC while the bell still rings; test it
|
|
# before attaching a session to it (LRN-148).
|
|
# Both are invisible no-ops in terminals that ignore them.
|
|
set -u
|
|
|
|
payload=$(cat 2>/dev/null)
|
|
read_field() {
|
|
printf '%s' "$payload" | jq -r "$1 // empty" 2>/dev/null \
|
|
| tr -d '\000-\037' | cut -c1-160
|
|
}
|
|
|
|
# How many background tasks are still running as the hook fires.
|
|
background_count() {
|
|
count=$(printf '%s' "$payload" | jq -r '(.background_tasks // []) | length' 2>/dev/null)
|
|
case "$count" in ''|*[!0-9]*) echo 0 ;; *) echo "$count" ;; esac
|
|
}
|
|
|
|
event=$(read_field '.notification_type')
|
|
[ -n "$event" ] || event=$(read_field '.hook_event_name')
|
|
|
|
|
|
|
|
case "$event" in
|
|
# Turn end while a subagent still runs is not the real end: stay silent,
|
|
# the next turn end will signal once the work is actually done.
|
|
Stop) [ "$(background_count)" -eq 0 ] || exit 0
|
|
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" ;;
|
|
# anything else (agent_completed, auth_success, quota_*) stays silent:
|
|
# signal only for turn end and moments needing the user.
|
|
*) exit 0 ;;
|
|
esac
|
|
|
|
detail=$(read_field '.message')
|
|
if [ -n "$detail" ]; then
|
|
# Claude Code's own wording often restates the label ("Claude needs your
|
|
# permission"). Append it only when it actually adds something.
|
|
short=$(printf '%s' "$detail" | tr '[:upper:]' '[:lower:]' | sed 's/^claude //')
|
|
case "$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')" in
|
|
*"$short"*) : ;;
|
|
*) label="${label}: ${detail}" ;;
|
|
esac
|
|
fi
|
|
|
|
bell=$(printf '\a')
|
|
esc=$(printf '\033')
|
|
seq="${bell}${bell}${esc}]777;notify;Claude Code;${label}${esc}\\"
|
|
jq -cn --arg seq "$seq" '{suppressOutput: true, terminalSequence: $seq}'
|
|
exit 0
|