← 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)"