mirror of
https://github.com/boxpositron/absolute-dotfiles.git
synced 2026-10-24 08:23:19 +00:00
66d1d21011
omo-notify and omo-remote played Apple's Glass and Ping through afplay, which exist only on macOS and are not ours to ship. They now play omo-done (task finished), omo-input (needs your input) and omo-error (stopped with an error), rendered by sfx from .config/omo/sounds/recipes.toml at the same loudness as Glass. The new omo-sound plays a sound by name or any audio file, with afplay on macOS and pw-play, paplay, aplay or ffplay on Linux, and finds the sounds through the dotfiles checkout it is linked from.
99 lines
4.0 KiB
Bash
Executable File
99 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Ding, tab marker and notification for omo (senpi) hooks: UserPromptSubmit,
|
|
# Stop and Notification. Reads the hook JSON payload on stdin. Wired up in
|
|
# ~/.omo/agent/hooks.json. The sound plays through omo-sound (afplay on macOS),
|
|
# so Focus modes and notification permissions cannot mute it; the banner is a
|
|
# silent extra.
|
|
# Ghostty posts the banner, so clicking it opens the tab it came from, and
|
|
# omo-status then selects the session's pane. omo-status also colours that tab:
|
|
# blue working, amber waiting for you, green finished, red error.
|
|
|
|
# Hooks run with a minimal environment, so make Homebrew tools reachable.
|
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
|
|
|
payload=$(cat)
|
|
field() { jq -r "$1 // empty" <<<"$payload" 2>/dev/null; }
|
|
|
|
# omo's UserPromptSubmit payload names the event only in "event".
|
|
event=$(field '.hook_event_name // .event')
|
|
project=$(basename "$(field '.cwd')")
|
|
|
|
# Only top-level sessions live under .omo/agent/sessions/. Subagents, team members
|
|
# and other background sessions keep transcripts elsewhere (e.g. senpi-task/children).
|
|
transcript=$(field '.transcript_path')
|
|
[[ -z "$transcript" || "$transcript" == */.omo/agent/sessions/* ]] || exit 0
|
|
|
|
message=""
|
|
sound=""
|
|
case "$event" in
|
|
UserPromptSubmit)
|
|
state=working
|
|
;;
|
|
Stop)
|
|
case "$(field '.stopReason')" in
|
|
# Esc/interrupts are your own doing; no need to be told about them.
|
|
aborted) state="clear" ;;
|
|
error) state="error" message="Stopped with an error" sound="omo-error" ;;
|
|
*) state="done" message="Task finished" sound="omo-done" ;;
|
|
esac
|
|
;;
|
|
Notification)
|
|
if [[ "$(field '.kind')" == "ask-user-asked" ]]; then
|
|
state=input message="Needs your input: $(field '.title')" sound="omo-input"
|
|
else
|
|
# Answered or timed out: the agent carries on.
|
|
state=working
|
|
fi
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
# Background the ding so the hook returns immediately; redirect output so the
|
|
# hook runner is not left waiting on the player's inherited pipes.
|
|
[[ -n "$sound" ]] && "$HOME/.local/bin/omo-sound" "$sound" >/dev/null 2>&1 &
|
|
|
|
# Find the tmux pane omo runs in: hooks run detached without TMUX_PANE, so walk
|
|
# up the process tree to the first ancestor whose tty is a tmux pane.
|
|
# Outside tmux, the first tty found is the Ghostty tab's own terminal.
|
|
# omo-remote names the pane itself (OMO_NOTIFY_PANE): for a session on a remote
|
|
# server it is the local pane running mosh or ssh.
|
|
pane="${OMO_NOTIFY_PANE:-}"
|
|
own_tty=""
|
|
panes=$(tmux list-panes -a -F '#{pane_tty} #{pane_id}' 2>/dev/null)
|
|
pid=$PPID
|
|
while [[ -z "$pane" && "${pid:-1}" -gt 1 ]]; do
|
|
tty=$(ps -o tty= -p "$pid" | tr -d ' ')
|
|
if [[ -n "$tty" && "$tty" != "??" ]]; then
|
|
[[ -n "$own_tty" ]] || own_tty="/dev/$tty"
|
|
[[ -n "$panes" ]] && pane=$(awk -v t="/dev/$tty" '$1 == t { print $2 }' <<<"$panes")
|
|
fi
|
|
pid=$(ps -o ppid= -p "$pid" | tr -d ' ')
|
|
done
|
|
|
|
[[ -n "$pane" ]] && "$HOME/.local/bin/omo-status" set "$pane" "$state"
|
|
[[ -n "$message" ]] || exit 0
|
|
|
|
log="${OMO_NOTIFY_LOG:-$HOME/.local/state/omo-notify.log}"
|
|
mkdir -p "$(dirname "$log")"
|
|
printf '%s %s %s %s [%s %s] pane=%s\n' "$(date '+%F %T')" "$event" "$project" "$message" \
|
|
"$(field '.session_id')" "$(field '.transcript_path')" "${pane:-none}" >>"$log"
|
|
|
|
# The banner is an OSC 777 notification for Ghostty. In tmux it goes to the tab's
|
|
# tmux client terminal, because tmux does not pass it on from a pane, and
|
|
# @omo_jump tells omo-status which pane to select when that tab gets focus.
|
|
target=$own_tty
|
|
if [[ -n "$pane" ]]; then
|
|
session=$(tmux display-message -p -t "$pane" '#{session_id}')
|
|
target=$(tmux list-clients -t "$session" -F '#{client_activity} #{client_tty}' | sort -rn | awk 'NR == 1 { print $2 }')
|
|
tmux set-option -t "$session" @omo_jump "$pane"
|
|
fi
|
|
# Control characters would end the escape sequence early; ";" separates its fields.
|
|
clean() { tr -d '\000-\037\177' <<<"$1" | tr ';' ','; }
|
|
if [[ -n "$target" && -w "$target" ]]; then
|
|
printf '\033]777;notify;%s;%s\a' "$(clean "omo: ${project:-session}")" "$(clean "$message")" >"$target"
|
|
fi
|
|
|
|
exit 0
|