← back to Terminal Status
verification/install/before/.claude/skills/color/scripts/tab-state.sh
171 lines
#!/usr/bin/env bash
# tab-state.sh — drive the session-close visual state machine on the Claude
# Code terminal tab. States:
# red solid red — session closing (/cs)
# flash-red blinking red — compaction in progress (PreCompact hook)
# flash-orange blinking orange — STOPPED, waiting on a background result
# (e.g. a background task / DTD panel / long job to land)
# flash-purple blinking purple — STOPPED & WAITING ON STEVE (Stop hook): the
# (alias: turn has been handed back and Claude is idle awaiting input.
# waiting) Cleared by a `stop`/`green`/`red` (or the next UserPromptSubmit).
# purple solid purple
# flash-green green flash ×3 then solid green — compaction complete (SessionStart compact)
# green solid green
# stop kill any running flasher
#
# Reuses /color's mechanism: Claude Code's Bash tool has no controlling tty, so
# OSC escapes on stdout never reach the terminal. Instead we locate the `claude`
# process's own pty by walking process ancestry and write the OSC tab-tint code
# straight to /dev/ttysNNN. OSC color codes are non-destructive (no cursor moves)
# so writing them — even repeatedly from a background flasher — is glitch-safe.
set -u
MODE="${1:-}"
# Streamline / lite mode — skip cosmetic tab-state painting fleet-wide, but
# always allow 'stop' so a running flasher can still be cleaned up. rm to restore.
case "$MODE" in stop) : ;; *) [ -f "$HOME/.claude/hooks-lite" ] && exit 0 ;; esac
PIDFILE="${HOME}/.claude/tab-flash.pid"
# Walk ancestry from $$ to find the controlling ttys* (same as color.sh).
resolve_tty() {
local p=$$ line pp tt
for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
line="$(ps -o ppid=,tty= -p "$p" 2>/dev/null)" || break
# shellcheck disable=SC2086
set -- $line; pp="${1:-}"; tt="${2:-}"
[[ -z "$pp" ]] && break
if [[ "$tt" == ttys* ]]; then echo "/dev/$tt"; return 0; fi
p="$pp"
done
return 1
}
# write_color <tty> <R> <G> <B> — tint the iTerm2 tab (tmux-wrapped if needed)
write_color() {
local tty="$1" R="$2" G="$3" B="$4"
[[ -w "$tty" ]] || return 1
if [[ -n "${TMUX:-}" ]]; then
{ printf '\033Ptmux;\033\033]6;1;bg;red;brightness;%d\007\033\\' "$R"
printf '\033Ptmux;\033\033]6;1;bg;green;brightness;%d\007\033\\' "$G"
printf '\033Ptmux;\033\033]6;1;bg;blue;brightness;%d\007\033\\' "$B"
} > "$tty" 2>/dev/null
else
{ printf '\033]6;1;bg;red;brightness;%d\007' "$R"
printf '\033]6;1;bg;green;brightness;%d\007' "$G"
printf '\033]6;1;bg;blue;brightness;%d\007' "$B"
} > "$tty" 2>/dev/null
fi
}
# reset_color <tty> — clear the tab tint back to iTerm2 default (the "off" half
# of a blink)
reset_color() {
local tty="$1"
[[ -w "$tty" ]] || return 1
if [[ -n "${TMUX:-}" ]]; then
printf '\033Ptmux;\033\033]6;1;bg;*;default\007\033\\' > "$tty" 2>/dev/null
else
printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
fi
}
stop_flasher() {
if [[ -f "$PIDFILE" ]]; then
kill "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null || true
rm -f "$PIDFILE"
fi
}
RED=(255 0 0)
GREEN=(0 200 0)
PURPLE=(148 0 211)
case "$MODE" in
red)
stop_flasher
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
write_color "$tty" "${RED[@]}" && echo "tab → solid RED (closing)"
;;
green)
stop_flasher
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
write_color "$tty" "${GREEN[@]}" && echo "tab → solid GREEN (done)"
;;
purple)
stop_flasher
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
write_color "$tty" "${PURPLE[@]}" && echo "tab → solid PURPLE (stopped, waiting on Steve)"
;;
flash-red)
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
stop_flasher
# Resolve tty in the parent, then hand it to the detached loop — once
# backgrounded the loop may be reparented and lose the ancestry walk.
( trap 'exit 0' TERM INT
while :; do
printf '\033]6;1;bg;red;brightness;255\007\033]6;1;bg;green;brightness;0\007\033]6;1;bg;blue;brightness;0\007' > "$tty" 2>/dev/null
sleep 0.5
printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
sleep 0.5
done ) >/dev/null 2>&1 &
echo $! > "$PIDFILE"
disown 2>/dev/null || true
echo "tab → FLASHING RED (compacting) pid $(cat "$PIDFILE")"
;;
flash-orange)
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
stop_flasher
# STOPPED, waiting on a background result. Blink orange (255,140,0) until
# something kills the flasher (a `stop`/`green`/`red`, or work resumes).
( trap 'exit 0' TERM INT
while :; do
printf '\033]6;1;bg;red;brightness;255\007\033]6;1;bg;green;brightness;140\007\033]6;1;bg;blue;brightness;0\007' > "$tty" 2>/dev/null
sleep 0.5
printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
sleep 0.5
done ) >/dev/null 2>&1 &
echo $! > "$PIDFILE"
disown 2>/dev/null || true
echo "tab → FLASHING ORANGE (stopped, waiting on background result) pid $(cat "$PIDFILE")"
;;
flash-purple|waiting)
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
stop_flasher
# STOPPED & WAITING ON STEVE — the turn was handed back and Claude is idle
# awaiting input. Blink purple (148,0,211) until something kills the flasher
# (a `stop`/`green`/`red`, or the next UserPromptSubmit when Steve replies).
# Resolve tty in the parent, then hand it to the detached loop — once
# backgrounded the loop may be reparented and lose the ancestry walk.
( trap 'exit 0' TERM INT
while :; do
printf '\033]6;1;bg;red;brightness;148\007\033]6;1;bg;green;brightness;0\007\033]6;1;bg;blue;brightness;211\007' > "$tty" 2>/dev/null
sleep 0.5
printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
sleep 0.5
done ) >/dev/null 2>&1 &
echo $! > "$PIDFILE"
disown 2>/dev/null || true
echo "tab → FLASHING PURPLE (stopped, waiting on Steve) pid $(cat "$PIDFILE")"
;;
flash-green)
stop_flasher
tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
for _ in 1 2 3; do
write_color "$tty" "${GREEN[@]}"; sleep 0.3
reset_color "$tty"; sleep 0.3
done
write_color "$tty" "${GREEN[@]}"
echo "tab → GREEN flash ×3 then solid (compaction complete)"
;;
stop)
stop_flasher
echo "tab-state: flasher stopped"
;;
*)
echo "usage: tab-state.sh {red|green|purple|flash-red|flash-orange|flash-purple|waiting|flash-green|stop}"
exit 2
;;
esac