#!/usr/bin/env bash
# send-message.sh — reliably deliver a message to a tmux pane running an
# interactive REPL (e.g. a Claude Code / Codex agent).
#
# WHY THIS EXISTS
#   Pasting multi-line text into an interactive agent REPL via `tmux send-keys`
#   is unreliable: the text lands in the input box but a single trailing Enter
#   in the same keystroke stream is frequently swallowed, so the message sits as
#   an UNSUBMITTED DRAFT ("Press up to edit queued messages") and the agent never
#   sees it. The mechanical fix is: paste as a bracketed paste (so embedded
#   newlines don't submit early), pause, then send Enter as its OWN keystroke,
#   pause, and send Enter again to flush. An extra Enter on an empty prompt is a
#   no-op in Claude Code, so the double-Enter is safe.
#
# USAGE
#   send-message.sh [-L socket_name] -t <target> -m "message"
#   send-message.sh [-L socket_name] -t <target> -f <file>
#   echo "message" | send-message.sh [-L socket_name] -t <target>
#   ssh host bash -s -- -L socket -t <target> -b "$(base64 -w0 <<<msg)" < send-message.sh
#
# OPTIONS
#   -L NAME     tmux socket name passed to `tmux -L NAME` (optional)
#   -t TARGET   tmux target: session, or session:window.pane  [required]
#   -m MESSAGE  message text (single- or multi-line)
#   -f FILE     read message from FILE instead of -m
#   -b BASE64   message as base64 (ssh-safe transport; decoded internally)
#   -r N        Enter-flush attempts (default 2)
#   -v          verbose: print a short tail of the pane after delivery
#   -h          help
#
# EXIT CODES
#   0  delivered (submitted) or queued (agent busy; will process when free)
#   1  tmux target not found
#   2  message still appears to be an unsubmitted draft after retries
#   3  usage error
set -uo pipefail

SOCKET_NAME=""; TARGET=""; MSG=""; FILE=""; B64=""; RETRIES=2; VERBOSE=0
usage() { sed -n '2,34p' "$0"; exit "${1:-3}"; }

while getopts "L:t:m:f:b:r:vh" o; do
  case "$o" in
    L) SOCKET_NAME=$OPTARG ;;
    t) TARGET=$OPTARG ;; m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; b) B64=$OPTARG ;;
    r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
  esac
done

[ -n "$TARGET" ] || { echo "ERROR: -t TARGET is required" >&2; usage 3; }
if   [ -n "$B64" ];  then MSG=$(printf '%s' "$B64" | base64 -d) || { echo "ERROR: bad -b base64" >&2; exit 3; }
elif [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE")
elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat)
fi
[ -n "$MSG" ] || { echo "ERROR: empty message (use -m, -f, or stdin)" >&2; exit 3; }

tmux_cmd=(tmux)
if [ -n "$SOCKET_NAME" ]; then
  tmux_cmd+=(-L "$SOCKET_NAME")
fi

# tmux accepts `=session` for some commands, but pane-level commands such as
# capture-pane require a pane-qualified target. Keep exact-session addressing
# convenient while avoiding accidental prefix matches.
EFFECTIVE_TARGET=$TARGET
if [[ "$TARGET" == =* && "$TARGET" != *:* ]]; then
  EFFECTIVE_TARGET="${TARGET}:0.0"
fi

# Target must resolve to a live pane.
if ! "${tmux_cmd[@]}" list-panes -t "$EFFECTIVE_TARGET" >/dev/null 2>&1; then
  echo "ERROR: tmux target not found: $TARGET" >&2; exit 1
fi

QUEUED_RE='Press up to edit queued messages'
# A distinctive tail of the message to spot an unsubmitted draft on the input line.
snippet=$(printf '%s' "$MSG" | tr '\n' ' ' | tr -s ' ' | sed 's/[^[:print:]]//g' | tail -c 32)

# 1) Paste the body as a bracketed paste so multi-line content does not submit
#    line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
# Buffer name MUST be unique per invocation: concurrent senders on the shared
# tmux server race a fixed name (load overwrites load, -d deletes underneath),
# cross-delivering or dropping messages — bit the fleet on the 2026-07-09
# simultaneous restart (briefs swapped between sessions).
BUF="__mosaic_send_$$_$(date +%s%N)"
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
# -p = bracketed paste when the client supports it; fall back if not.
"${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \
  || "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET" \
  || "${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null
# ^ -d deletes the buffer only on a SUCCESSFUL paste; if both attempts fail
#   (e.g. the target vanished since the liveness check), delete explicitly —
#   named buffers are exempt from tmux's buffer-limit eviction, so orphans
#   would otherwise accumulate forever.
sleep 0.5

# 2) Submit, then verify; flush with another Enter if it is still a draft.
status="sent"
for attempt in $(seq 1 $((RETRIES + 1))); do
  "${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter
  sleep 1.2
  pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null)

  if printf '%s' "$pane" | grep -qF "$QUEUED_RE"; then
    status="queued"; break
  fi
  # Draft heuristic: the prompt glyph line still carries our message tail.
  # (Submitted messages scroll up into history; a draft stays on the ❯ line.)
  promptline=$(printf '%s' "$pane" | grep -E '❯|^>|│ >' | tail -1)
  if [ -n "$snippet" ] && printf '%s' "$promptline" | grep -qF "$snippet"; then
    status="draft"; continue
  fi
  status="delivered"; break
done

[ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; }

case "$status" in
  delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
  queued)    echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
  draft)     echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
  *)         echo "✓ sent to $TARGET (submission state indeterminate; verify with -v)"; exit 0 ;;
esac
