mirror of
https://github.com/boxpositron/absolute-dotfiles.git
synced 2026-10-24 08:23:19 +00:00
c9059d4867
mosh drops the OSC 777 banner that omo-notify posts, so omo sessions on the server only reached the phone through ntfy. The server's omo-notify now writes the session state into its tmux client's terminal title (omo:<state>:<time>:<project>), which mosh and ssh carry. A pane-title-changed hook hands it to the new omo-remote, which runs omo-notify for the local mosh pane through the new OMO_NOTIFY_PANE override, so remote sessions get the same sound, tab marker and banner as local ones. Sessions that no terminal is showing come through ntfy instead: omo-remote install starts a launchd agent running ntfy subscribe, which plays the sound and posts the banner for omo pushes the server did not tag attached. Brewfile gains ntfy; the token lives in ~/.config/ntfy/omo-remote.env, outside the repository.
98 lines
4.0 KiB
Bash
Executable File
98 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 afplay, 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="Glass" ;;
|
|
*) state="done" message="Task finished" sound="Glass" ;;
|
|
esac
|
|
;;
|
|
Notification)
|
|
if [[ "$(field '.kind')" == "ask-user-asked" ]]; then
|
|
state=input message="Needs your input: $(field '.title')" sound="Ping"
|
|
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 afplay's inherited pipes.
|
|
[[ -n "$sound" ]] && afplay "/System/Library/Sounds/${sound}.aiff" >/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
|