From a74af5e613b9bec5e0c3c67f7ab72534d9bbb32a Mon Sep 17 00:00:00 2001 From: Natalie Date: Wed, 27 May 2026 18:05:13 -0600 Subject: [PATCH] =?UTF-8?q?feat(send):=20=E2=9C=A8=20add=20slash-command?= =?UTF-8?q?=20mode=20for=20precise=20input=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- bin/rclaude | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/bin/rclaude b/bin/rclaude index 6ce1427..5e94bf0 100755 --- a/bin/rclaude +++ b/bin/rclaude @@ -808,8 +808,31 @@ cmd_list() { # rclaude send --match -- # rclaude send ... --yes -- # actually send (default: preview) # rclaude send ... --dry-run -- # explicit preview (overrides --yes) +# rclaude send ... --slash -- /foo bar # slash-command mode (see below) +# +# Input-buffer contract (important — read before debugging missing sends): +# +# Delivery uses `tmux send-keys -l` followed by `Enter`. tmux types into +# whatever buffer is currently focused in the target pane — it has no notion +# of "Claude prompt-ready". Two consequences: +# +# 1. If Claude is mid-turn or already has buffered text in its input area +# (queued user input, a partial draft), the new text is APPENDED. The +# single Enter submits the merged blob as ONE user message. To stop +# that, every send first clears the input line (`C-a C-k`) before +# typing the payload. +# +# 2. Slash commands like `/remote-control claire-plum` only fire when they +# are the ENTIRE user message (leading `/`, no surrounding text). Use +# `--slash` to enforce this: the text must begin with `/`, must not +# contain newlines, and the line-clear is mandatory. Without --slash, +# a leading `/` is just text — easy to accidentally turn a slash +# command into prose. +# +# Concurrent sends to the same pane can still race the input buffer. Callers +# that need ordering should serialize externally (flock on a per-pane lock). cmd_send() { - _sel=""; _pat=""; _dry=0; _yes=0 + _sel=""; _pat=""; _dry=0; _yes=0; _slash=0 while [ $# -gt 0 ]; do case $1 in --all) [ -n "$_sel" ] && { echo "rclaude send: only one selector allowed" >&2; exit 2; } @@ -824,6 +847,7 @@ cmd_send() { _sel=match; _pat=${1#--match=}; shift ;; --dry-run) _dry=1; shift ;; --yes) _yes=1; shift ;; + --slash) _slash=1; shift ;; --) shift; break ;; -*) echo "rclaude send: unknown flag: $1" >&2; exit 2 ;; *) break ;; @@ -832,12 +856,16 @@ cmd_send() { if [ -z "$_sel" ]; then cat >&2 <<'EOF' -usage: rclaude send (--all | --host | --match ) [--dry-run] [--yes] -- +usage: rclaude send (--all | --host | --match ) [--dry-run] [--yes] [--slash] -- --all target every live claude-* tmux session on scan_hosts --host target every claude-* session on a specific host --match substring match against tmux session name (which embeds slug) --dry-run preview targets and exit (default unless --yes is passed) --yes actually deliver (still prints preview first) + --slash slash-command mode: text must start with '/', no newlines; + the input line is force-cleared before the payload so the + command lands solo (any buffered text is discarded). Use + this for /remote-control, /clear, etc. EOF exit 2 fi @@ -848,6 +876,17 @@ EOF exit 2 fi + if [ "$_slash" = 1 ]; then + case $_text in + /*) ;; + *) echo "rclaude send --slash: text must start with '/' (got: $_text)" >&2; exit 2 ;; + esac + case $_text in + *"$(printf '\n')"*) + echo "rclaude send --slash: text must not contain newlines" >&2; exit 2 ;; + esac + fi + # Gather candidate rows across all hosts, then filter. _rows=$(scan_hosts | while IFS= read -r _h; do list_tmux_on "$_h"; done \ | filter_targets "$_sel" "$_pat")