← back to Terminal Status
verification/install/before/.claude/skills/tab-ticket/scripts/tab-ticket-title.sh
116 lines
#!/usr/bin/env bash
# tab-ticket-title.sh — set EVERY open `claude` OR `codex` window/tab TITLE
# (top of the window) to "TK-#### · up Xh Ym" so at a glance you know which
# ticket each window is working AND how long it's been up. (2026-09-09: added
# codex coverage — Steve runs claude+codex peer sessions side by side on the
# same tickets, and codex ttys were silently falling back to iTerm's generic
# "<dir> (codex)" auto-title with no ticket shown at all.)
#
# Mechanism (reuses /color's proven path): both tools' Bash/exec runs detached
# with NO controlling tty, so OSC escapes on stdout never reach the terminal.
# We enumerate every live `claude`/`codex` process's controlling ttysNNN (same
# as color.sh --all) and write the OSC TITLE escape straight to /dev/ttysNNN.
#
# CRITICAL: the title OSC ( ESC ]0; / ]1; / ]2; ) is INDEPENDENT of the
# color-tint OSC ( ESC ]6;1;bg;... ). Writing a title does NOT clear the tab
# color — verified. So this can run every 60s without disturbing /color's tint.
#
# UPTIME — derived per-tty from the claude process's OWN start time via
# `ps -o etime= -p <pid>`. No state file needed.
# TICKET — read from ~/.claude/tab-tickets/<ttyname>.txt (just the TK id).
# Declared by a session via tab-ticket-set.sh. Missing → "no-TK".
set -u
TICKET_DIR="${HOME}/.claude/tab-tickets"
mkdir -p "$TICKET_DIR"
# fmt_uptime <etime> — turn ps etime ([[DD-]HH:]MM:SS) into "Xd Yh" / "Xh Ym" / "Xm"
fmt_uptime() {
awk -v e="$1" 'BEGIN{
days=0; rest=e
n=split(e, a, "-")
if (n==2){ days=a[1]+0; rest=a[2] } else { rest=a[1] }
m=split(rest, t, ":")
if (m==3){ h=t[1]+0; mi=t[2]+0 }
else if (m==2){ h=0; mi=t[1]+0 }
else { h=0; mi=0 }
# total hours including days
th = days*24 + h
if (days > 0) printf "up %dd %dh", days, h
else if (th > 0) printf "up %dh %dm", th, mi
else printf "up %dm", mi
}'
}
# Enumerate every live claude OR codex session's controlling tty. Both report
# comm=node / their own binary, so match on the full command line (same as
# color.sh --all). codex sessions never call tab-ticket-set.sh, so we also
# pull a TK-#### straight out of the launch command line itself (it's right
# there in argv for every ticket-driven run — "TK-10670-onboard-refresh-...")
# as a fallback when no manually-declared ticket file exists. This covers
# BOTH claude and codex ttys — claude tabs that never called tab-ticket-set.sh
# now also pick up their ticket from argv instead of sitting on "no-TK".
# Capture tty + etime + auto-detected-TK in one ps pass so we pick the RIGHT
# process per tty. Written for bash 3.2 (macOS /bin/bash under launchd): NO
# mapfile, NO associative arrays. One session may report multiple matching
# rows on the same tty (helper procs); awk collapses to the FIRST row seen
# per tty.
ROWS="$(ps -axo tty=,pid=,etime=,command= 2>/dev/null \
| awk '$1 ~ /^ttys/ && $0 ~ /[c]laude|[c]odex/ && !seen[$1]++ {
tk = "";
if (match($0, /TK-[0-9]+/)) tk = substr($0, RSTART, RLENGTH);
print $1"\t"$3"\t"tk
}')"
if [[ -z "$ROWS" ]]; then
echo "tab-ticket-title → no live claude/codex ttys found"
exit 0
fi
n=0
while IFS=$'\t' read -r tt etime autotk; do
[[ -z "$tt" ]] && continue
tp="/dev/$tt"
[[ -w "$tp" ]] || continue
# ticket for this tty: manually-declared file wins, else fall back to
# whatever TK-#### was found in the process's own launch command.
tk="no-TK"
tkfile="$TICKET_DIR/${tt}.txt"
if [[ -f "$tkfile" ]]; then
v="$(tr -d ' \t\r\n' < "$tkfile" 2>/dev/null)"
[[ -n "$v" ]] && tk="$v"
fi
if [[ "$tk" == "no-TK" && -n "$autotk" ]]; then tk="$autotk"; fi
up="$(fmt_uptime "$etime")"
label="${tk} · ${up}"
# Semantic DOT prefix (2026-09-02): /orangedot /purpledot /yellowdot /pinkdot drop
# "<emoji> <label>" into ~/.claude/tab-dots/<tty>.dot; --off removes it. Prefixing
# it here means the dot SURVIVES this 60s repaint (before, the repaint erased it),
# and it shows in the WINDOW TITLE even when the tab bar is hidden (single-tab windows).
dotfile="${HOME}/.claude/tab-dots/${tt}.dot"
d=""
if [[ -f "$dotfile" ]]; then
d="$(head -c 80 "$dotfile" 2>/dev/null | tr -d '\r\n')"
fi
# DEFAULT DOT (2026-09-02): a live claude tab with NO explicit state file gets a
# neutral ⚪ so NO tab is ever dotless at the top (root fix for "where is my dot?" —
# a brand-new session had no ~/.claude/tab-dots/<tty>.dot yet). The semantic dot
# skills (/pinkdot /orangedot /yellowdot /purpledot) still override by writing the file.
[[ -z "$d" ]] && d="⚪"
label="${d} · ${label}"
# Title-only OSC — set icon-name(1), window-title(2), and generic title(0).
# These are INDEPENDENT of the ]6;bg color-tint OSC, so the tab COLOR survives.
{
printf '\033]1;%s\007' "$label"
printf '\033]2;%s\007' "$label"
printf '\033]0;%s\007' "$label"
} > "$tp" 2>/dev/null && n=$(( n + 1 ))
done <<< "$ROWS"
echo "tab-ticket-title → set title on ${n} claude window(s)"