mirror of
https://github.com/boxpositron/absolute-dotfiles.git
synced 2026-10-24 08:23:19 +00:00
feat(omo): coloured tab markers and clickable Ghostty banners
omo-notify now drives three signals from omo's UserPromptSubmit, Stop and Notification hooks: - A sound through afplay, so Focus modes cannot mute it - A Ghostty notification (OSC 777) written to the session's tmux client terminal, so clicking it opens the tab it came from. This replaces the osascript banner, which opened Script Editor's file picker on click - omo-status markers in front of the tab title: red error, amber waiting for input, green finished, blue working. A tab shows its most urgent agent; focusing it clears red, amber and green and selects the pane that sent the banner tmux puts @omo_status in the title and runs omo-status on client focus.
This commit is contained in:
+68
-26
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# macOS notification for omo (senpi) Stop and Notification hooks.
|
||||
# Reads the hook JSON payload on stdin. Wired up in ~/.omo/agent/hooks.json.
|
||||
# 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"
|
||||
@@ -11,42 +16,79 @@ field() { jq -r "$1 // empty" <<<"$payload" 2>/dev/null; }
|
||||
event=$(field '.hook_event_name')
|
||||
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)
|
||||
# Esc/interrupts are your own doing; no need to be told about them.
|
||||
[[ "$(field '.stopReason')" == "aborted" ]] && exit 0
|
||||
# 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
|
||||
if [[ "$(field '.stopReason')" == "error" ]]; then
|
||||
message="Stopped with an error"
|
||||
else
|
||||
message="Task finished"
|
||||
fi
|
||||
sound="Glass"
|
||||
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)
|
||||
# Only alert when a question appears, not when it is answered or times out.
|
||||
[[ "$(field '.kind')" == "ask-user-asked" ]] || exit 0
|
||||
message="Needs your input: $(field '.title')"
|
||||
sound="Ping"
|
||||
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.
|
||||
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]\n' "$(date '+%F %T')" "$event" "$project" "$message" \
|
||||
"$(field '.session_id')" "$(field '.transcript_path')" >>"$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"
|
||||
|
||||
# Pass values as argv so quotes in titles cannot break the AppleScript.
|
||||
osascript - "omo: ${project:-session}" "$message" "$sound" <<'APPLESCRIPT' >/dev/null 2>&1
|
||||
on run argv
|
||||
display notification (item 2 of argv) with title (item 1 of argv) sound name (item 3 of argv)
|
||||
end run
|
||||
APPLESCRIPT
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user