← back to Terminal Status
verification/install/before/.claude/skills/colordots/paint-lib.sh
76 lines
#!/usr/bin/env bash
# paint-lib.sh — the ONE safe path for tinting an iTerm2 tab via an OSC write.
#
# Fixes the 2026-09-08 "dot payload lands in the Claude input field" bug. Two proven
# root causes (reproduced in a throwaway iTerm window, not on a live pane):
#
# 1. SUBAGENT CROSS-WRITE. The Claude Bash tool runs with no controlling tty, so the
# old ancestry walk climbed past the tty-less `zsh -c` shells to the OWNING `claude`
# process's ttysNNN. From the top-level session that resolves to the right pane, but
# from a SPAWNED SUBAGENT (Agent tool) it resolves to the PARENT session's pty — so a
# subagent painting a dot wrote straight into Steve's live pane. A subagent has no tab
# of its own; it must never paint. (CLAUDE_CODE_CHILD_SESSION=1 marks a subagent.)
#
# 2. BACKGROUND PULSE LOOP. The dot painters started a background loop that rewrote the
# tab-color OSC every ~0.11s forever. Those rapid async writes interleave with the
# Claude TUI's own pty output; a torn/desynced sequence drops its leading ESC]6;
# framing and iTerm renders the tail ("1;bg;green;brightness;0") as literal text on the
# Claude screen — exactly the reported symptom. A SINGLE atomic write can't be torn.
#
# A clean single OSC write was verified to set the tab color AND to never enter a
# raw-mode reader's stdin — so painting once, atomically, is both correct and leak-free.
TABDOTS_DIR="${HOME}/.claude/tab-dots"
# True (0) iff running inside a spawned subagent — MUST NOT paint (no tab of its own;
# any write lands in the parent session's live pane).
tabdots_is_subagent() { [ -n "${CLAUDE_CODE_CHILD_SESSION:-}" ]; }
# Resolve THIS session's controlling pty by climbing ancestry to the owning `claude`
# process's ttys. Prints "/dev/ttysNNN" on success. Returns non-zero (and prints nothing)
# when called from a subagent or when no ttys is found — callers must treat that as "do not paint".
tabdots_resolve_tty() {
tabdots_is_subagent && return 1
local p=$$ line pp tt
for _ in $(seq 1 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
case "$tt" in ttys*) printf '/dev/%s' "$tt"; return 0;; esac
p="$pp"
done
return 1
}
# Kill any legacy background pulse loop on a tty (the old corruption engine) and drop its pidfile.
tabdots_stop_pulse() {
local tp="$1" pf
pf="${TABDOTS_DIR}/$(basename "$tp").pulse.pid"
[ -f "$pf" ] && { kill "$(cat "$pf" 2>/dev/null)" 2>/dev/null; rm -f "$pf"; }
return 0
}
# Paint a tab in ONE atomic write() so a concurrent writer can never tear it.
# $1 tty_path $2 R $3 G $4 B $5 title $6 off(1=reset)
# No background loop — solid color only. Refuses in a subagent context.
tabdots_paint() {
local tp="$1" R="$2" G="$3" B="$4" title="$5" off="${6:-0}"
tabdots_is_subagent && return 2
[ -w "$tp" ] || return 1
tabdots_stop_pulse "$tp"
if [ "$off" = "1" ]; then
# Single write: reset tab color + clear badge.
printf '\033]6;1;bg;*;default\007\033]1337;SetBadgeFormat=\a' > "$tp" 2>/dev/null
rm -f "${TABDOTS_DIR}/$(basename "$tp").dot" 2>/dev/null
return 0
fi
local badge; badge="$(printf '%s' "$title" | base64 | tr -d '\n')"
# ONE write(): 3 tab-color OSCs + both title channels + the in-window badge, concatenated.
printf '\033]6;1;bg;red;brightness;%d\007\033]6;1;bg;green;brightness;%d\007\033]6;1;bg;blue;brightness;%d\007\033]1;%s\007\033]2;%s\007\033]1337;SetBadgeFormat=%s\a' \
"$R" "$G" "$B" "$title" "$title" "$badge" > "$tp" 2>/dev/null
mkdir -p "$TABDOTS_DIR"
printf '%s' "$title" > "${TABDOTS_DIR}/$(basename "$tp").dot"
return 0
}