feat(@scripts): ✨ add session enumeration mode
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
9c3c38a6db
commit
f79954e7fc
3 changed files with 275 additions and 55 deletions
|
|
@ -1,8 +1,12 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Internal helper for rclaude — prints one tab-separated line per Claude
|
# Internal helper for rclaude — enumerates Claude Code state from
|
||||||
# project directory under ~/.claude/projects/, sorted by most recent first.
|
# ~/.claude/projects/. Two modes:
|
||||||
#
|
#
|
||||||
# Output columns: <mtime_epoch>\t<cwd>\t<session_count>
|
# (default) one tab-separated row per project directory
|
||||||
|
# columns: <mtime_epoch>\t<cwd>\t<session_count>
|
||||||
|
#
|
||||||
|
# --sessions one tab-separated row per session jsonl
|
||||||
|
# columns: <mtime_epoch>\t<session_uuid>\t<cwd>\t<snippet>
|
||||||
#
|
#
|
||||||
# Used by `rclaude list` and `rclaude resume` to discover sessions that exist
|
# Used by `rclaude list` and `rclaude resume` to discover sessions that exist
|
||||||
# on disk but have no live tmux session attached. Run locally or invoked
|
# on disk but have no live tmux session attached. Run locally or invoked
|
||||||
|
|
@ -10,41 +14,146 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
root = Path.home() / ".claude" / "projects"
|
# Don't dump a traceback when piped into `head`.
|
||||||
if not root.is_dir():
|
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
rows = []
|
ROOT = Path.home() / ".claude" / "projects"
|
||||||
for project_dir in root.iterdir():
|
|
||||||
if not project_dir.is_dir():
|
|
||||||
continue
|
|
||||||
jsonls = sorted(project_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
||||||
if not jsonls:
|
|
||||||
continue
|
|
||||||
|
|
||||||
latest = jsonls[0]
|
# Reading every jsonl on a host with thousands of past sessions is slow. Cap
|
||||||
cwd = None
|
# the per-session listing to the N most recently modified jsonls.
|
||||||
|
SESSION_LIMIT = int(os.environ.get("CLAUDE_PROJECTS_LIMIT", "500"))
|
||||||
|
|
||||||
|
# Lines that look system-injected rather than user-typed. When picking the
|
||||||
|
# "name" of a session for fuzzy matching we want the first real user line.
|
||||||
|
SYSTEM_PREFIXES = (
|
||||||
|
"<command-name>", "<command-message>", "<system-reminder>",
|
||||||
|
"<local-command-", "<bash-input>", "<bash-stdout>",
|
||||||
|
"Caveat: The messages below",
|
||||||
|
"[task-persistence]", "[tts-state]", "[VERIFY:",
|
||||||
|
"This session is being continued", "Please continue",
|
||||||
|
"[result]", "<task-notification>", "[Request interrupted",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def looks_system(text: str) -> bool:
|
||||||
|
s = (text or "").lstrip()
|
||||||
|
if not s:
|
||||||
|
return True
|
||||||
|
return s.startswith(SYSTEM_PREFIXES)
|
||||||
|
|
||||||
|
|
||||||
|
def message_text(entry) -> str:
|
||||||
|
msg = entry.get("message") or {}
|
||||||
|
content = msg.get("content")
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts = []
|
||||||
|
for block in content:
|
||||||
|
if isinstance(block, dict) and block.get("type") == "text":
|
||||||
|
parts.append(block.get("text", ""))
|
||||||
|
return " ".join(parts)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def first_user_snippet(jsonl: Path) -> str:
|
||||||
|
"""Return a one-line snippet of the first non-system user message."""
|
||||||
try:
|
try:
|
||||||
with latest.open() as f:
|
with jsonl.open("r", encoding="utf-8", errors="replace") as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
role = (entry.get("message") or {}).get("role") or entry.get("type")
|
||||||
|
if role != "user":
|
||||||
|
continue
|
||||||
|
text = message_text(entry)
|
||||||
|
if looks_system(text):
|
||||||
|
continue
|
||||||
|
snippet = re.sub(r"\s+", " ", text).strip()
|
||||||
|
return snippet[:120]
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def session_cwd(jsonl: Path) -> str | None:
|
||||||
|
try:
|
||||||
|
with jsonl.open("r", encoding="utf-8", errors="replace") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
try:
|
try:
|
||||||
entry = json.loads(line)
|
entry = json.loads(line)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
continue
|
continue
|
||||||
if entry.get("cwd"):
|
if entry.get("cwd"):
|
||||||
cwd = entry["cwd"]
|
return entry["cwd"]
|
||||||
break
|
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
return None
|
||||||
if not cwd:
|
return None
|
||||||
continue
|
|
||||||
|
|
||||||
mtime = int(latest.stat().st_mtime)
|
|
||||||
rows.append((mtime, cwd, len(jsonls)))
|
|
||||||
|
|
||||||
rows.sort(reverse=True)
|
def list_projects():
|
||||||
for mtime, cwd, count in rows:
|
if not ROOT.is_dir():
|
||||||
print(f"{mtime}\t{cwd}\t{count}")
|
return
|
||||||
|
rows = []
|
||||||
|
for project_dir in ROOT.iterdir():
|
||||||
|
if not project_dir.is_dir():
|
||||||
|
continue
|
||||||
|
jsonls = sorted(
|
||||||
|
project_dir.glob("*.jsonl"),
|
||||||
|
key=lambda p: p.stat().st_mtime,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
if not jsonls:
|
||||||
|
continue
|
||||||
|
cwd = session_cwd(jsonls[0])
|
||||||
|
if not cwd:
|
||||||
|
continue
|
||||||
|
mtime = int(jsonls[0].stat().st_mtime)
|
||||||
|
rows.append((mtime, cwd, len(jsonls)))
|
||||||
|
rows.sort(reverse=True)
|
||||||
|
for mtime, cwd, count in rows:
|
||||||
|
print(f"{mtime}\t{cwd}\t{count}")
|
||||||
|
|
||||||
|
|
||||||
|
def list_sessions():
|
||||||
|
if not ROOT.is_dir():
|
||||||
|
return
|
||||||
|
# First pass: cheap stat-only collection, then keep the N most-recent
|
||||||
|
# to bound the second (expensive) jsonl-parse pass.
|
||||||
|
candidates = []
|
||||||
|
for project_dir in ROOT.iterdir():
|
||||||
|
if not project_dir.is_dir():
|
||||||
|
continue
|
||||||
|
for jsonl in project_dir.glob("*.jsonl"):
|
||||||
|
try:
|
||||||
|
mtime = int(jsonl.stat().st_mtime)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
candidates.append((mtime, jsonl))
|
||||||
|
candidates.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
for mtime, jsonl in candidates[:SESSION_LIMIT]:
|
||||||
|
uuid = jsonl.stem
|
||||||
|
cwd = session_cwd(jsonl) or ""
|
||||||
|
if not cwd:
|
||||||
|
continue
|
||||||
|
snippet = first_user_snippet(jsonl).replace("\t", " ")
|
||||||
|
print(f"{mtime}\t{uuid}\t{cwd}\t{snippet}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if args and args[0] == "--sessions":
|
||||||
|
list_sessions()
|
||||||
|
else:
|
||||||
|
list_projects()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
|
||||||
155
bin/rclaude
155
bin/rclaude
|
|
@ -14,8 +14,14 @@
|
||||||
# Permission mode: --dangerously-skip-permissions is on by default. Override
|
# Permission mode: --dangerously-skip-permissions is on by default. Override
|
||||||
# with RCLAUDE_PERMS=default (or any --permission-mode value).
|
# with RCLAUDE_PERMS=default (or any --permission-mode value).
|
||||||
#
|
#
|
||||||
# Hosts scanned by `list`/`resume` default to: local + apricot. Override with
|
# Tmux config sync: the repo's canonical tmux.conf is pushed to the target
|
||||||
# RCLAUDE_HOSTS="apricot black quinn-vps".
|
# host's ~/.tmux.d/session-tools.conf on every launch and source-lined from
|
||||||
|
# ~/.tmux.conf. Disable with RCLAUDE_SYNC_TMUX=0, or set =once to only write
|
||||||
|
# if the source-line isn't already present.
|
||||||
|
#
|
||||||
|
# Hosts scanned by `list`/`resume` default to: local + apricot + plum (the
|
||||||
|
# non-local one is dialed; the local one is rendered as "local"). Override
|
||||||
|
# with RCLAUDE_HOSTS="apricot black quinn-vps".
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# rclaude # local, $PWD
|
# rclaude # local, $PWD
|
||||||
|
|
@ -23,8 +29,11 @@
|
||||||
# rclaude <host> # remote: $PWD mirrored under remote $HOME
|
# rclaude <host> # remote: $PWD mirrored under remote $HOME
|
||||||
# rclaude <host> . # same as above (explicit form)
|
# rclaude <host> . # same as above (explicit form)
|
||||||
# rclaude <host> <dir> # remote (or local) at <dir>
|
# rclaude <host> <dir> # remote (or local) at <dir>
|
||||||
# rclaude list # show active sessions across hosts
|
# rclaude list # tmux + per-project disk view
|
||||||
# rclaude resume [pattern] # reattach (interactive if pattern matches >1)
|
# rclaude list sessions # tmux + per-session disk view (uuid + snippet)
|
||||||
|
# rclaude resume [pattern] # reattach / resume by uuid prefix, snippet,
|
||||||
|
# # tmux name, or cwd substring (interactive
|
||||||
|
# # picker on >1 match)
|
||||||
#
|
#
|
||||||
# Mirror semantics: if local $PWD is $HOME/X/Y, the remote dir defaults to
|
# Mirror semantics: if local $PWD is $HOME/X/Y, the remote dir defaults to
|
||||||
# ~/X/Y on the remote (the remote's $HOME, not $HOME from this machine).
|
# ~/X/Y on the remote (the remote's $HOME, not $HOME from this machine).
|
||||||
|
|
@ -108,23 +117,74 @@ list_disk_on() {
|
||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Combined enumeration: tmux first (live), then on-disk.
|
# List on-disk Claude sessions per UUID on a host (via _claude-projects --sessions).
|
||||||
|
# Output one row per session jsonl:
|
||||||
|
# <host>\tsession\t<uuid>\t<snippet>\t<cwd> · <relative-time>
|
||||||
|
list_sessions_on() {
|
||||||
|
_host=$1
|
||||||
|
_helper_dir=$(dirname "$(resolve_self)")
|
||||||
|
if is_local "$_host"; then
|
||||||
|
_raw=$("$_helper_dir/_claude-projects" --sessions 2>/dev/null || true)
|
||||||
|
else
|
||||||
|
_raw=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "$_host" 'python3 - --sessions' < "$_helper_dir/_claude-projects" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
_now=$(date +%s)
|
||||||
|
printf %s "$_raw" | awk -F'\t' -v host="$_host" -v now="$_now" '
|
||||||
|
function rel(secs, abs, s) {
|
||||||
|
abs = (secs < 0) ? -secs : secs
|
||||||
|
if (abs < 60) s = abs " seconds"
|
||||||
|
else if (abs < 3600) s = int(abs/60) " min"
|
||||||
|
else if (abs < 86400) s = int(abs/3600) " hours"
|
||||||
|
else s = int(abs/86400) " days"
|
||||||
|
return s " ago"
|
||||||
|
}
|
||||||
|
NF >= 3 {
|
||||||
|
snippet = ($4 == "" ? "(no user text)" : $4)
|
||||||
|
printf "%s\tsession\t%s\t%s\t%s · %s\n", host, $2, snippet, $3, rel(now - $1)
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Combined enumeration: tmux first (live), then on-disk per-project.
|
||||||
list_all_on() {
|
list_all_on() {
|
||||||
list_tmux_on "$1"
|
list_tmux_on "$1"
|
||||||
list_disk_on "$1"
|
list_disk_on "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resume-search enumeration: tmux + per-session UUIDs/snippets.
|
||||||
|
list_search_on() {
|
||||||
|
list_tmux_on "$1"
|
||||||
|
list_sessions_on "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# Push the canonical session-tools tmux fragment to <host> and ensure
|
# Push the canonical session-tools tmux fragment to <host> and ensure
|
||||||
# ~/.tmux.conf sources it. Idempotent; runs on every launch so config changes
|
# ~/.tmux.conf sources it. Idempotent; runs on every launch so config changes
|
||||||
# in the repo propagate without re-running install.sh on each host. Silent
|
# in the repo propagate without re-running install.sh on each host.
|
||||||
# no-op if the repo fragment can't be located.
|
#
|
||||||
|
# Controlled by RCLAUDE_SYNC_TMUX:
|
||||||
|
# 1 (default) auto-sync on every launch
|
||||||
|
# 0 never sync (e.g. host has a hand-tuned tmux config you don't
|
||||||
|
# want clobbered by a stray source-file line)
|
||||||
|
# once sync only if the source-file line isn't already present;
|
||||||
|
# useful on hosts where you've audited the fragment once and
|
||||||
|
# don't want repeated writes
|
||||||
|
#
|
||||||
|
# Silent no-op if the repo fragment can't be located.
|
||||||
sync_tmux_conf() {
|
sync_tmux_conf() {
|
||||||
_host=$1
|
_host=$1
|
||||||
|
_mode=${RCLAUDE_SYNC_TMUX:-1}
|
||||||
|
case $_mode in
|
||||||
|
0|off|no|false) return 0 ;;
|
||||||
|
esac
|
||||||
_self=$(resolve_self)
|
_self=$(resolve_self)
|
||||||
_repo=$(cd "$(dirname "$_self")/.." 2>/dev/null && pwd)
|
_repo=$(cd "$(dirname "$_self")/.." 2>/dev/null && pwd)
|
||||||
_frag="$_repo/tmux.conf"
|
_frag="$_repo/tmux.conf"
|
||||||
[ -f "$_frag" ] || return 0
|
[ -f "$_frag" ] || return 0
|
||||||
_remote_cmd='mkdir -p ~/.tmux.d && cat > ~/.tmux.d/session-tools.conf && { grep -q "session-tools.conf" ~/.tmux.conf 2>/dev/null || printf "source-file ~/.tmux.d/session-tools.conf\n" >> ~/.tmux.conf; } && tmux source-file ~/.tmux.conf 2>/dev/null || true'
|
_guard=""
|
||||||
|
case $_mode in
|
||||||
|
once) _guard='grep -q "session-tools.conf" ~/.tmux.conf 2>/dev/null && exit 0;' ;;
|
||||||
|
esac
|
||||||
|
_remote_cmd="${_guard} "'mkdir -p ~/.tmux.d && cat > ~/.tmux.d/session-tools.conf && { grep -q "session-tools.conf" ~/.tmux.conf 2>/dev/null || printf "source-file ~/.tmux.d/session-tools.conf\n" >> ~/.tmux.conf; } && tmux source-file ~/.tmux.conf 2>/dev/null || true'
|
||||||
if is_local "$_host"; then
|
if is_local "$_host"; then
|
||||||
sh -c "$_remote_cmd" < "$_frag" 2>/dev/null || true
|
sh -c "$_remote_cmd" < "$_frag" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
|
|
@ -132,10 +192,14 @@ sync_tmux_conf() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# All hosts to scan for list/resume.
|
# All hosts to scan for list/resume. Defaults to "apricot plum" so resume
|
||||||
|
# discovery works symmetrically from either host (the local one is rendered
|
||||||
|
# as "local" and remote ones are filtered to drop any host that matches the
|
||||||
|
# current machine).
|
||||||
scan_hosts() {
|
scan_hosts() {
|
||||||
printf "local\n"
|
printf "local\n"
|
||||||
for h in ${RCLAUDE_HOSTS:-apricot}; do
|
for h in ${RCLAUDE_HOSTS:-apricot plum}; do
|
||||||
|
is_local "$h" && continue
|
||||||
printf "%s\n" "$h"
|
printf "%s\n" "$h"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
@ -145,28 +209,44 @@ scan_hosts() {
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
_mode=${1:-all} # all | tmux | disk
|
_mode=${1:-all} # all | tmux | disk | sessions
|
||||||
printf "%-10s %-6s %-60s %s\n" "HOST" "KIND" "SESSION/CWD" "DETAIL"
|
printf "%-10s %-7s %-60s %s\n" "HOST" "KIND" "SESSION/CWD/UUID" "DETAIL"
|
||||||
scan_hosts | while IFS= read -r h; do
|
scan_hosts | while IFS= read -r h; do
|
||||||
case $_mode in
|
case $_mode in
|
||||||
tmux) list_tmux_on "$h" ;;
|
tmux) list_tmux_on "$h" ;;
|
||||||
disk) list_disk_on "$h" ;;
|
disk) list_disk_on "$h" ;;
|
||||||
*) list_all_on "$h" ;;
|
sessions|--sessions)
|
||||||
esac | awk -F'\t' '{printf "%-10s %-6s %-60s %s\n", $1, $2, $3, $4}'
|
list_tmux_on "$h"
|
||||||
|
list_sessions_on "$h" ;;
|
||||||
|
*) list_all_on "$h" ;;
|
||||||
|
esac | awk -F'\t' '{
|
||||||
|
# Tmux/disk rows: $3 is the display target. Session rows: $3=uuid,
|
||||||
|
# $4=snippet (show snippet, abbreviate uuid into DETAIL via $5).
|
||||||
|
if ($2 == "session") {
|
||||||
|
uuid_short = substr($3, 1, 8)
|
||||||
|
detail = (NF >= 5 ? $5 : "") " [" uuid_short "]"
|
||||||
|
printf "%-10s %-7s %-60.60s %s\n", $1, $2, $4, detail
|
||||||
|
} else {
|
||||||
|
printf "%-10s %-7s %-60.60s %s\n", $1, $2, $3, $4
|
||||||
|
}
|
||||||
|
}'
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resume strategy:
|
# Resume strategy:
|
||||||
# - 1 match → attach directly
|
# - 1 match → attach directly
|
||||||
# - 2+ matches → single-key picker (1-9 then a-z, max 35)
|
# - 2+ matches → single-key picker (1-9 then a-z, max 35)
|
||||||
# - matches a tmux row → ssh+tmux attach (preserves the live conversation)
|
# - matches a tmux row → ssh+tmux attach (preserves the live conversation)
|
||||||
# - matches a disk row → re-exec self with `<host> <cwd>` so the normal
|
# - matches a session UUID → ssh+tmux+claude --resume <uuid> at recorded cwd
|
||||||
# launch path spins up a fresh tmux + claude --continue
|
# - matches a snippet/cwd → same as session (the row identifies a UUID)
|
||||||
|
#
|
||||||
|
# Pattern matching is case-insensitive substring across host/kind/uuid/snippet/cwd.
|
||||||
|
# An empty pattern lists everything (interactive picker).
|
||||||
cmd_resume() {
|
cmd_resume() {
|
||||||
_pattern=${1:-}
|
_pattern=${1:-}
|
||||||
_matches=$(scan_hosts | while IFS= read -r h; do list_all_on "$h"; done)
|
_matches=$(scan_hosts | while IFS= read -r h; do list_search_on "$h"; done)
|
||||||
if [ -n "$_pattern" ]; then
|
if [ -n "$_pattern" ]; then
|
||||||
_matches=$(printf '%s\n' "$_matches" | grep -F -- "$_pattern" || true)
|
_matches=$(printf '%s\n' "$_matches" | grep -F -i -- "$_pattern" || true)
|
||||||
fi
|
fi
|
||||||
_count=0
|
_count=0
|
||||||
[ -n "$_matches" ] && _count=$(printf '%s\n' "$_matches" | wc -l | tr -d ' ')
|
[ -n "$_matches" ] && _count=$(printf '%s\n' "$_matches" | wc -l | tr -d ' ')
|
||||||
|
|
@ -180,9 +260,13 @@ cmd_resume() {
|
||||||
echo "too many matches ($_count); refine pattern" >&2
|
echo "too many matches ($_count); refine pattern" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# For sessions, display the human-readable snippet (col 4) rather
|
||||||
|
# than the bare UUID (col 3); for tmux/disk the existing col 3 is
|
||||||
|
# already the right thing to show.
|
||||||
|
_fmt_row='function display(){ if ($2 == "session") return $4 " [" substr($3,1,8) "]"; return $3 } { printf "%-10s %-7s %s", $1, $2, display() }'
|
||||||
if [ ! -t 0 ] || [ ! -t 2 ]; then
|
if [ ! -t 0 ] || [ ! -t 2 ]; then
|
||||||
echo "multiple matches and no tty for picker; refine pattern:" >&2
|
echo "multiple matches and no tty for picker; refine pattern:" >&2
|
||||||
printf '%s\n' "$_matches" | awk -F'\t' '{printf " %-10s %-6s %s\n", $1, $2, $3}' >&2
|
printf '%s\n' "$_matches" | awk -F'\t' "$_fmt_row"'{printf "\n"}' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
_i=0
|
_i=0
|
||||||
|
|
@ -190,7 +274,7 @@ cmd_resume() {
|
||||||
_i=$((_i + 1))
|
_i=$((_i + 1))
|
||||||
_k=$(printf %s "$_keys" | cut -c"$_i")
|
_k=$(printf %s "$_keys" | cut -c"$_i")
|
||||||
printf ' [%s] %s\n' "$_k" \
|
printf ' [%s] %s\n' "$_k" \
|
||||||
"$(printf %s "$_line" | awk -F'\t' '{printf "%-10s %-4s %s", $1, $2, $3}')" >&2
|
"$(printf %s "$_line" | awk -F'\t' "$_fmt_row")" >&2
|
||||||
done
|
done
|
||||||
_last_key=$(printf %s "$_keys" | cut -c"$_count")
|
_last_key=$(printf %s "$_keys" | cut -c"$_count")
|
||||||
printf 'select [1-%s]: ' "$_last_key" >&2
|
printf 'select [1-%s]: ' "$_last_key" >&2
|
||||||
|
|
@ -216,6 +300,9 @@ cmd_resume() {
|
||||||
_host=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $1}')
|
_host=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $1}')
|
||||||
_kind=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $2}')
|
_kind=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $2}')
|
||||||
_target=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $3}')
|
_target=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $3}')
|
||||||
|
# Sessions carry their cwd in column 5 (formatted "<cwd> · <rel-time>");
|
||||||
|
# extract the cwd half for the launch dir.
|
||||||
|
_session_cwd=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $5}' | awk -F' · ' '{print $1}')
|
||||||
case $_kind in
|
case $_kind in
|
||||||
tmux)
|
tmux)
|
||||||
if is_local "$_host"; then
|
if is_local "$_host"; then
|
||||||
|
|
@ -225,11 +312,17 @@ cmd_resume() {
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
disk)
|
disk)
|
||||||
# Spawn tmux + claude --continue at the recorded cwd. The env var
|
# Spawn tmux + claude --continue at the recorded cwd.
|
||||||
# tells the launch path below to add --continue (it doesn't by
|
|
||||||
# default, since --continue is the explicit-resume behavior).
|
|
||||||
RCLAUDE_RESUME=1 exec "$0" "$_host" "$_target"
|
RCLAUDE_RESUME=1 exec "$0" "$_host" "$_target"
|
||||||
;;
|
;;
|
||||||
|
session)
|
||||||
|
# Spawn tmux + claude --resume <uuid> at the session's recorded cwd.
|
||||||
|
if [ -z "$_session_cwd" ]; then
|
||||||
|
echo "rclaude: session $_target has no recorded cwd" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
RCLAUDE_RESUME_ID=$_target exec "$0" "$_host" "$_session_cwd"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +409,11 @@ build_inner() {
|
||||||
# creates a new uniquely-named tmux session. Reattach to a live session
|
# creates a new uniquely-named tmux session. Reattach to a live session
|
||||||
# via `rclaude resume <pattern>`; disk-resume after host death likewise.
|
# via `rclaude resume <pattern>`; disk-resume after host death likewise.
|
||||||
_resume_flag=""
|
_resume_flag=""
|
||||||
[ "${RCLAUDE_RESUME:-0}" = "1" ] && _resume_flag="--continue"
|
if [ -n "${RCLAUDE_RESUME_ID:-}" ]; then
|
||||||
|
_resume_flag="--resume ${RCLAUDE_RESUME_ID}"
|
||||||
|
elif [ "${RCLAUDE_RESUME:-0}" = "1" ]; then
|
||||||
|
_resume_flag="--continue"
|
||||||
|
fi
|
||||||
printf '%s' \
|
printf '%s' \
|
||||||
"cd ${1} && rc_t=\$(date +%s); claude ${_resume_flag} ${flag}; rc_ec=\$?; " \
|
"cd ${1} && rc_t=\$(date +%s); claude ${_resume_flag} ${flag}; rc_ec=\$?; " \
|
||||||
"rc_e=\$(date +%s); rc_d=\$((rc_e - rc_t)); " \
|
"rc_e=\$(date +%s); rc_d=\$((rc_e - rc_t)); " \
|
||||||
|
|
|
||||||
14
tmux.conf
14
tmux.conf
|
|
@ -15,3 +15,17 @@ set -g history-limit 50000
|
||||||
# passed as arrow keys to the inner app (which would scroll Claude history etc.)
|
# passed as arrow keys to the inner app (which would scroll Claude history etc.)
|
||||||
# Hold Option on macOS to bypass tmux and use native terminal selection.
|
# Hold Option on macOS to bypass tmux and use native terminal selection.
|
||||||
set -g mouse on
|
set -g mouse on
|
||||||
|
|
||||||
|
# Resize panes to the active client's size, not the smallest attached. Matters
|
||||||
|
# when reattaching the same durable session from different terminals (phone,
|
||||||
|
# laptop, IDE pane) — otherwise the layout collapses to the smallest viewer.
|
||||||
|
setw -g aggressive-resize on
|
||||||
|
|
||||||
|
# Forward copy-mode selections to the OS clipboard via OSC 52, so mouse-drag
|
||||||
|
# or `y` in copy mode actually lands in the local terminal's clipboard even
|
||||||
|
# across ssh + tmux.
|
||||||
|
set -g set-clipboard on
|
||||||
|
|
||||||
|
# Keep window indexes contiguous when one closes; quiet the activity flag spam.
|
||||||
|
set -g renumber-windows on
|
||||||
|
setw -g monitor-activity off
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue