feat(send): add slash-command mode for precise input control

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-27 18:05:13 -06:00
parent f11e820d86
commit a74af5e613

View file

@ -808,8 +808,31 @@ cmd_list() {
# rclaude send --match <pat> -- <text...>
# rclaude send ... --yes -- <text...> # actually send (default: preview)
# rclaude send ... --dry-run -- <text...> # 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 <h> | --match <pat>) [--dry-run] [--yes] -- <text...>
usage: rclaude send (--all | --host <h> | --match <pat>) [--dry-run] [--yes] [--slash] -- <text...>
--all target every live claude-* tmux session on scan_hosts
--host <h> target every claude-* session on a specific host
--match <pat> 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")