Add tab re-ordering.

This commit is contained in:
Greyson Parrelli
2026-08-14 21:21:13 -04:00
parent 695417a601
commit b50e2f4108
4 changed files with 332 additions and 19 deletions
+59 -16
View File
@@ -26,6 +26,59 @@ set -u
state=${1:-idle}
# ---------------------------------------------------------------------------
# Read the event.
#
# Claude Code hands the hook its event as JSON on stdin. Both of the states
# that care about it read it here, before anything has been written, because
# the payload decides what — if anything — this call should report at all.
#
# Guarded on a tty so that running this by hand still works: stdin is then the
# keyboard, and `cat` would sit there waiting for a payload nobody is going to
# type.
payload=""
if [ "$state" = busy ] || [ "$state" = input ]; then
[ -t 0 ] || payload=$(cat 2>/dev/null) || payload=""
fi
# Pull a string field out of the payload. jq when it is there, and otherwise a
# sed fallback that gives up on a value containing an escaped quote — the
# common case handled badly rather than the rare case handled wrongly. Both
# callers have a safe default for the empty answer.
json_str() {
[ -n "$payload" ] || return 0
if command -v jq >/dev/null 2>&1; then
printf '%s' "$payload" | jq -r --arg k "$1" '.[$k] // ""' 2>/dev/null
else
printf '%s' "$payload" \
| sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
fi
}
# The Notification event is not one event. Claude Code raises it for permission
# prompts, for elicitations, for auth results, for a subagent finishing — and
# for `idle_prompt`, a 60-second timer that fires whenever a session has been
# sitting with nothing running. Reporting every one of them as "blocked on
# you" is what makes a finished, cleared session light up amber a minute after
# you have walked away from it.
#
# So report the kind, not the event. An absent field means a caller that isn't
# Claude Code, which keeps the argument's plain meaning.
if [ "$state" = input ]; then
case $(json_str notification_type) in
permission_prompt | elicitation_dialog | agent_needs_input | "") ;;
# The opposite claim: the session is sitting idle. Worth saying, because it
# clears a stale amber left by a prompt that was answered somewhere else.
idle_prompt) state=idle ;;
# Informational — auth results, a completed elicitation, a subagent
# finishing while the session itself is still working. None of them change
# whether the pane wants you, so leave it showing whatever it shows.
*) exit 0 ;;
esac
fi
# ---------------------------------------------------------------------------
# Find the pty to write to.
#
@@ -114,25 +167,15 @@ printf '\033]9;4;%s\007' "$code" > "$tty_target" 2>/dev/null || true
# Only a starting turn carries a task worth naming. The other states leave the
# title alone so the shell's own title comes back when the session ends.
[ "$state" = busy ] || exit 0
payload=$(cat 2>/dev/null) || exit 0
[ -n "$payload" ] || exit 0
if command -v jq >/dev/null 2>&1; then
title=$(printf '%s' "$payload" | jq -r '.prompt // ""' 2>/dev/null) || title=""
else
# No jq — pull the field out directly. This gives up on a prompt whose first
# line contains an escaped quote, which is the common case handled badly
# rather than the rare case handled wrongly: a missed title costs nothing
# because the tab just keeps the name it already had.
title=$(printf '%s' "$payload" \
| sed -n 's/.*"prompt"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
title=$(json_str prompt)
# Still JSON at this point, so a newline is the two characters \ and n
# rather than an actual line break. `head` below would not split on it and
# the whole prompt would arrive as one long title with \n sitting in it.
title=${title%%\\n*}
fi
# Without jq the title is still JSON at this point, so a newline is the two
# characters \ and n rather than an actual line break. `head` below would not
# split on it and the whole prompt would arrive as one long title with \n
# sitting in it. Harmless on the jq path, which has already unescaped them.
title=${title%%\\n*}
# First line only, and no control characters: an OSC string ends at the first
# BEL or ESC, so anything of that sort in a prompt would truncate the sequence