agent tracker + tmux update

This commit is contained in:
David Chen 2026-03-25 12:13:26 -07:00
parent cd9c92b1c2
commit 5064629d61
68 changed files with 15041 additions and 3483 deletions

View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
target="${1:-}"
window_id="${2:-}"
if [[ -z "$target" ]]; then
echo "usage: $(basename "$0") <left|top-right|bottom-right> [window_id]" >&2
exit 1
fi
if [[ -z "$window_id" ]]; then
window_id="$(tmux display-message -p '#{window_id}')"
fi
tmux list-panes -t "$window_id" -F '#{pane_id} #{pane_left} #{pane_top}' | python3 -c '
import sys
target = sys.argv[1]
panes = []
for line in sys.stdin:
line = line.strip()
if not line:
continue
pane_id, left, top = line.split()
panes.append((pane_id, int(left), int(top)))
if not panes:
sys.exit(1)
if target == "left":
chosen = min(panes, key=lambda p: (p[1], p[2], p[0]))
elif target == "top-right":
max_left = max(p[1] for p in panes)
candidates = [p for p in panes if p[1] == max_left]
chosen = min(candidates, key=lambda p: (p[2], p[0]))
elif target == "bottom-right":
max_left = max(p[1] for p in panes)
candidates = [p for p in panes if p[1] == max_left]
chosen = max(candidates, key=lambda p: (p[2], p[0]))
else:
sys.exit(2)
print(chosen[0])
' "$target"

View file

@ -1,11 +1,22 @@
#!/bin/bash
session_id=$(tmux new-session -d -P -F '#{session_id}' 2>/dev/null)
LOCK="/tmp/tmux-new-session.lock"
current_session_id="${1:-}"
touch "$LOCK"
session_id=$(tmux new-session -d -P -s 'session' -F '#{session_id}' 2>/dev/null)
if [ -z "$session_id" ]; then
rm -f "$LOCK"
exit 0
fi
python3 "$HOME/.config/tmux/scripts/session_manager.py" ensure
if [ -n "$current_session_id" ]; then
python3 "$HOME/.config/tmux/scripts/session_manager.py" insert-right "$current_session_id" "$session_id"
else
python3 "$HOME/.config/tmux/scripts/session_manager.py" ensure
fi
rm -f "$LOCK"
tmux switch-client -t "$session_id"

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
client_tty="${1-}"
window_id="${2-}"
agent_id="${3-}"
path_value="${4-}"
session_name="${5-}"
window_name="${6-}"
exec tmux display-popup -E -c "$client_tty" -d "$path_value" -w 78% -h 80% -T agent \
~/.config/agent-tracker/bin/agent palette \
--window="$window_id" \
--agent-id="$agent_id" \
--path="$path_value" \
--session-name="$session_name" \
--window-name="$window_name"

View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
RESTART_OP_SCRIPT="${TMUX_RESTART_OP_SCRIPT:-${XDG_CONFIG_HOME:-$HOME/.config}/tmux/scripts/restart_opencode_pane.sh}"
RESTORE_AGENT_RUN_PANES_SCRIPT="${TMUX_RESTORE_AGENT_RUN_PANES_SCRIPT:-${XDG_CONFIG_HOME:-$HOME/.config}/tmux/scripts/restore_agent_run_panes.py}"
RESTORE_AGENT_TRACKER_SCRIPT="${TMUX_RESTORE_AGENT_TRACKER_SCRIPT:-${XDG_CONFIG_HOME:-$HOME/.config}/tmux/scripts/restore_agent_tracker_mapping.py}"
resurrect_dir() {
if [[ -d "$HOME/.tmux/resurrect" ]]; then
printf '%s\n' "$HOME/.tmux/resurrect"
else
printf '%s\n' "${XDG_DATA_HOME:-$HOME/.local/share}/tmux/resurrect"
fi
}
last_file="${TMUX_RESURRECT_LAST_FILE:-$(resurrect_dir)/last}"
op_pane_locators() {
[[ -e "$last_file" ]] || return 0
awk -F '\t' '
$1 == "pane" && (index($7, "OpenCode") > 0 || index($11, "opencode") > 0) {
print $2 ":" $3 "." $6
}
' "$last_file" | awk '!seen[$0]++'
}
resume_opencode_panes() {
[[ -x "$RESTART_OP_SCRIPT" ]] || return 0
local locator pane_id
while IFS= read -r locator; do
[[ -n "$locator" ]] || continue
pane_id="$(tmux display-message -p -t "$locator" '#{pane_id}' 2>/dev/null || true)"
[[ -n "$pane_id" ]] || continue
"$RESTART_OP_SCRIPT" "$pane_id" >/dev/null 2>&1 || true
done < <(op_pane_locators)
}
restore_agent_tracker_mappings() {
[[ -x "$RESTORE_AGENT_TRACKER_SCRIPT" ]] || return 0
"$RESTORE_AGENT_TRACKER_SCRIPT" >/dev/null 2>&1 || true
}
restore_agent_run_panes() {
[[ -x "$RESTORE_AGENT_RUN_PANES_SCRIPT" ]] || return 0
"$RESTORE_AGENT_RUN_PANES_SCRIPT" >/dev/null 2>&1 || true
}
sleep 1
resume_opencode_panes
restore_agent_run_panes
restore_agent_tracker_mappings

View file

@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
pane_id="${1:-}"
if [[ -z "$pane_id" ]]; then
pane_id=$(tmux display-message -p '#{pane_id}')
fi
if [[ -z "$pane_id" ]]; then
exit 1
fi
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/op"
pane_locator=$(tmux display-message -p -t "$pane_id" '#{session_name}:#{window_index}.#{pane_index}' 2>/dev/null || true)
state_file="$state_dir/loc_${pane_locator//[^a-zA-Z0-9_]/_}"
if [[ -z "$pane_locator" ]]; then
tmux display-message "op-restart: unable to resolve pane locator"
exit 0
fi
if [[ ! -f "$state_file" ]]; then
tmux display-message "op-restart: no saved session for ${pane_locator}"
exit 0
fi
IFS= read -r session_id < "$state_file"
if [[ -z "$session_id" ]]; then
tmux display-message "op-restart: invalid session mapping for ${pane_locator}"
exit 0
fi
wait_for_shell() {
local cmd i
for ((i = 0; i < 30; i++)); do
cmd=$(tmux display-message -p -t "$pane_id" '#{pane_current_command}' 2>/dev/null || true)
case "$cmd" in
zsh|bash|sh|fish|nu)
return 0
;;
esac
sleep 0.1
done
return 1
}
kill_opencode_on_tty() {
local pane_tty tty_name pid
local -a pids=()
pane_tty=$(tmux display-message -p -t "$pane_id" '#{pane_tty}' 2>/dev/null || true)
if [[ -z "$pane_tty" ]]; then
return 0
fi
tty_name="${pane_tty#/dev/}"
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(ps -t "$tty_name" -o pid= -o args= 2>/dev/null | awk '/\/opt\/homebrew\/bin\/opencode|\/opt\/homebrew\/lib\/node_modules\/opencode-ai\/bin\/\.opencode|opencode-darwin-arm64\/bin\/opencode/ { print $1 }')
if [[ ${#pids[@]} -eq 0 ]]; then
return 0
fi
kill -TERM "${pids[@]}" 2>/dev/null || true
sleep 0.3
for pid in "${pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill -KILL "$pid" 2>/dev/null || true
fi
done
}
tmux send-keys -t "$pane_id" C-c
if ! wait_for_shell; then
kill_opencode_on_tty
if ! wait_for_shell; then
tmux display-message "op-restart: pane ${pane_id} did not return to shell"
exit 0
fi
fi
tmux send-keys -t "$pane_id" "OP_TRACKER_NOTIFY=1 op -s ${session_id}" C-m

View file

@ -0,0 +1,103 @@
#!/usr/bin/env python3
import os
import re
import shlex
import subprocess
import sys
from pathlib import Path
HOME = Path.home()
def resurrect_dir() -> Path:
legacy = HOME / ".tmux" / "resurrect"
if legacy.is_dir():
return legacy
data_home = Path(os.environ.get("XDG_DATA_HOME", str(HOME / ".local" / "share")))
return data_home / "tmux" / "resurrect"
LAST_FILE = Path(os.environ.get("TMUX_RESURRECT_LAST_FILE", str(resurrect_dir() / "last")))
def tmux_output(*args: str) -> str:
return subprocess.check_output(["tmux", *args], text=True).strip()
def tmux_run(*args: str) -> None:
subprocess.run(["tmux", *args], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def parse_device(command: str) -> str:
match = re.search(r"flutter run -d (?:\"([^\"]+)\"|'([^']+)'|(\S+))", command)
if not match:
return ""
for group in match.groups():
if group:
return group.strip()
return ""
def iter_targets():
if not LAST_FILE.exists():
return
with LAST_FILE.open() as handle:
for raw_line in handle:
parts = raw_line.rstrip("\n").split("\t")
if len(parts) < 11 or parts[0] != "pane":
continue
if parts[9].strip() != "script":
continue
workspace = parts[7].lstrip(":").strip()
if "/.agents/" not in workspace:
continue
device = parse_device(parts[10].lstrip(":").strip())
if not device:
continue
ensure_server = Path(workspace) / "ensure-server.sh"
if not ensure_server.is_file():
continue
locator = f"{parts[1]}:{parts[2]}.{parts[5]}"
yield locator, workspace, device
def main() -> int:
restored = 0
try:
targets = list(iter_targets())
except Exception as exc:
print(f"restore-agent-run-panes: {exc}", file=sys.stderr)
return 1
seen = set()
for locator, workspace, device in targets:
if locator in seen:
continue
seen.add(locator)
try:
current_command = tmux_output("display-message", "-p", "-t", locator, "#{pane_current_command}")
except Exception:
continue
if current_command.strip() != "script":
continue
command = f"cd {shlex.quote(workspace)} && ./ensure-server.sh {shlex.quote(device)}"
tmux_run("respawn-pane", "-k", "-t", locator, command)
restored += 1
if restored:
print(f"restored {restored} agent run pane(s)")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,212 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
from pathlib import Path
HOME = Path.home()
AGENTS_PATH = HOME / ".config/agent-tracker/run/agents.json"
TODOS_PATH = HOME / ".cache/agent/todos.json"
def tmux_output(*args: str) -> str:
return subprocess.check_output(["tmux", *args], text=True).strip()
def tmux_run(*args: str) -> None:
subprocess.run(["tmux", *args], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def load_json(path: Path):
with path.open() as handle:
return json.load(handle)
def save_json(path: Path, payload) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as handle:
json.dump(payload, handle, indent=2)
handle.write("\n")
def list_windows():
out = tmux_output("list-windows", "-a", "-F", "#{session_name}\t#{session_id}\t#{window_name}\t#{window_id}")
windows = []
for line in out.splitlines():
parts = line.split("\t")
if len(parts) != 4:
continue
windows.append(
{
"session_name": parts[0].strip(),
"session_id": parts[1].strip(),
"window_name": parts[2].lstrip(":").strip(),
"window_id": parts[3].strip(),
}
)
return windows
def list_panes():
out = tmux_output("list-panes", "-a", "-F", "#{window_id}\t#{pane_index}\t#{pane_id}\t#{pane_current_path}")
panes = []
for line in out.splitlines():
parts = line.split("\t", 3)
if len(parts) != 4:
continue
try:
pane_index = int(parts[1].strip())
except ValueError:
continue
panes.append(
{
"window_id": parts[0].strip(),
"pane_index": pane_index,
"pane_id": parts[2].strip(),
"path": parts[3].strip(),
}
)
return panes
def detect_agent_id_from_path(path: str) -> str:
clean = os.path.normpath(path.strip())
needle = f"{os.sep}.agents{os.sep}"
if needle not in clean:
return ""
rest = clean.split(needle, 1)[1]
if not rest:
return ""
return rest.split(os.sep, 1)[0].strip()
def merge_items(existing, incoming):
merged = list(existing)
merged.extend(incoming)
return merged
def main() -> int:
if not AGENTS_PATH.exists():
return 0
try:
agents_payload = load_json(AGENTS_PATH)
windows = list_windows()
panes = list_panes()
except Exception as exc:
print(f"restore-agent-tracker-mapping: {exc}", file=sys.stderr)
return 1
windows_by_key = {(w["session_name"], w["window_name"]): w for w in windows}
panes_by_window = {}
inferred_window_for_agent = {}
for pane in panes:
panes_by_window.setdefault(pane["window_id"], []).append(pane)
agent_id = detect_agent_id_from_path(pane["path"])
if agent_id and agent_id not in inferred_window_for_agent:
inferred_window_for_agent[agent_id] = pane["window_id"]
windows_by_id = {w["window_id"]: w for w in windows}
changed_agents = 0
state_changed = False
window_migrations = {}
session_migrations = {}
agents = agents_payload.get("agents", {})
for agent_id, record in agents.items():
session_name = str(record.get("tmux_session_name", "")).strip()
old_window_id = str(record.get("tmux_window_id", "")).strip()
old_session_id = str(record.get("tmux_session_id", "")).strip()
match = windows_by_key.get((session_name, agent_id))
if match is None:
inferred_window_id = inferred_window_for_agent.get(agent_id, "")
if inferred_window_id:
match = windows_by_id.get(inferred_window_id)
if match is None:
continue
new_window_id = match["window_id"]
new_session_id = match["session_id"]
new_session_name = match["session_name"]
if old_window_id and old_window_id != new_window_id:
window_migrations[old_window_id] = new_window_id
if old_session_id and old_session_id != new_session_id:
session_migrations[old_session_id] = new_session_id
if (
old_window_id != new_window_id
or old_session_id != new_session_id
or session_name != new_session_name
):
changed_agents += 1
state_changed = True
record["tmux_window_id"] = new_window_id
record["tmux_session_id"] = new_session_id
record["tmux_session_name"] = new_session_name
tmux_run("set-option", "-w", "-t", new_window_id, "@agent_id", agent_id)
panes_for_window = sorted(panes_by_window.get(new_window_id, []), key=lambda pane: pane["pane_index"])
if panes_for_window:
roles = ["ai", "git", "run"]
pane_record = dict(record.get("panes", {}))
for idx, role in enumerate(roles):
if idx >= len(panes_for_window):
break
pane_id = panes_for_window[idx]["pane_id"]
pane_record[role] = pane_id
tmux_run("set-option", "-p", "-t", pane_id, "@agent_role", role)
if pane_record != record.get("panes", {}):
state_changed = True
record["panes"] = pane_record
if state_changed:
save_json(AGENTS_PATH, agents_payload)
migrated_window_todos = 0
migrated_session_todos = 0
if TODOS_PATH.exists():
todos_payload = load_json(TODOS_PATH)
windows_store = todos_payload.setdefault("windows", {})
for old_id, new_id in window_migrations.items():
if old_id == new_id or old_id not in windows_store:
continue
existing = windows_store.get(new_id, [])
incoming = windows_store.pop(old_id)
windows_store[new_id] = merge_items(existing, incoming)
migrated_window_todos += len(incoming)
sessions_store = todos_payload.setdefault("sessions", {})
for old_id, new_id in session_migrations.items():
if old_id == new_id or old_id not in sessions_store:
continue
existing = sessions_store.get(new_id, [])
incoming = sessions_store.pop(old_id)
sessions_store[new_id] = merge_items(existing, incoming)
migrated_session_todos += len(incoming)
if migrated_window_todos or migrated_session_todos:
save_json(TODOS_PATH, todos_payload)
summary = []
if changed_agents:
summary.append(f"{changed_agents} agent windows")
if migrated_window_todos:
summary.append(f"{migrated_window_todos} window todos")
if migrated_session_todos:
summary.append(f"{migrated_session_todos} session todos")
if summary:
print("restored " + ", ".join(summary))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Resurrect strategy for `op` (opencode).
# Called by tmux-resurrect as: script <pane_full_command> <dir>
# Returns the command to run in the pane.
pane_full_command="$1"
session_name=$(tmux display-message -p '#{session_name}' 2>/dev/null || true)
window_index=$(tmux display-message -p '#{window_index}' 2>/dev/null || true)
pane_index=$(tmux display-message -p '#{pane_index}' 2>/dev/null || true)
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/op"
if [[ -n "$session_name" && -n "$window_index" && -n "$pane_index" ]]; then
locator="${session_name}:${window_index}.${pane_index}"
key="${locator//[^a-zA-Z0-9_]/_}"
loc_file="$state_dir/loc_${key}"
if [[ -f "$loc_file" ]]; then
session_id=$(cat "$loc_file")
if [[ -n "$session_id" ]]; then
echo "OP_TRACKER_NOTIFY=1 op -s ${session_id}"
exit 0
fi
fi
fi
echo "OP_TRACKER_NOTIFY=1 op"

View file

@ -1,3 +1,8 @@
#!/bin/bash
LOCK="/tmp/tmux-new-session.lock"
if [ -f "$LOCK" ]; then
exit 0
fi
python3 "$HOME/.config/tmux/scripts/session_manager.py" created

View file

@ -120,6 +120,26 @@ def command_move(direction: str) -> None:
apply_order(sessions)
def command_insert_right(anchor_id: str, moving_id: str) -> None:
if not anchor_id or not moving_id or anchor_id == moving_id:
command_ensure()
return
sessions = list_sessions()
indices = {session["id"]: idx for idx, session in enumerate(sessions)}
if anchor_id not in indices or moving_id not in indices:
command_ensure()
return
moving_session = sessions.pop(indices[moving_id])
anchor_pos = next((idx for idx, session in enumerate(sessions) if session["id"] == anchor_id), None)
if anchor_pos is None:
sessions.append(moving_session)
else:
sessions.insert(anchor_pos + 1, moving_session)
apply_order(sessions)
def command_ensure() -> None:
sessions = list_sessions()
if sessions:
@ -161,6 +181,8 @@ def main(argv: List[str]) -> None:
command_rename(argv[2])
elif command == "move" and len(argv) >= 3:
command_move(argv[2])
elif command == "insert-right" and len(argv) >= 4:
command_insert_right(argv[2], argv[3])
elif command == "ensure":
command_ensure()
elif command == "created":

View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
direction="${1:-}"
case "$direction" in
left|right) ;;
*) exit 0 ;;
esac
session_id=$(tmux display-message -p '#{session_id}' 2>/dev/null || true)
window_id=$(tmux display-message -p '#{window_id}' 2>/dev/null || true)
[[ -n "$session_id" && -n "$window_id" ]] || exit 0
windows=()
while IFS= read -r window; do
[[ -n "$window" ]] && windows+=("$window")
done < <(tmux list-windows -t "$session_id" -F '#{window_id}' 2>/dev/null || true)
count=${#windows[@]}
(( count >= 2 )) || exit 0
current=-1
for i in "${!windows[@]}"; do
if [[ "${windows[$i]}" == "$window_id" ]]; then
current=$i
break
fi
done
(( current >= 0 )) || exit 0
if [[ "$direction" == "left" ]]; then
target_index=$(( current == 0 ? count - 1 : current - 1 ))
else
target_index=$(( current == count - 1 ? 0 : current + 1 ))
fi
target_window_id="${windows[$target_index]}"
[[ -n "$target_window_id" && "$target_window_id" != "$window_id" ]] || exit 0
tmux swap-window -d -s "$window_id" -t "$target_window_id"

View file

@ -6,20 +6,16 @@ window_id="$2"
[[ -z "$pane_id" || -z "$window_id" ]] && exit 1
shells="bash zsh fish sh dash ksh tcsh csh"
pane_pid=$(tmux display-message -p -t "$pane_id" '#{pane_pid}' 2>/dev/null || true)
[[ -z "$pane_pid" ]] && exit 0
is_shell() {
local cmd="$1"
for s in $shells; do
[[ "$cmd" == "$s" ]] && return 0
done
return 1
}
pane_shell=$(ps -o comm= -p "$pane_pid" 2>/dev/null | sed 's|.*/||; s/^-//')
[[ -z "$pane_shell" ]] && exit 0
current_cmd=$(tmux display-message -p -t "$pane_id" '#{pane_current_command}' 2>/dev/null || true)
[[ -z "$current_cmd" ]] && exit 0
if is_shell "$current_cmd"; then
if [[ "$current_cmd" == "$pane_shell" ]]; then
exit 0
fi
@ -31,7 +27,7 @@ while true; do
watching=$(tmux show -wv -t "$window_id" @watching 2>/dev/null || true)
[[ "$watching" != "1" ]] && exit 0
cmd=$(tmux display-message -p -t "$pane_id" '#{pane_current_command}' 2>/dev/null || true)
if [[ -z "$cmd" ]] || is_shell "$cmd"; then
if [[ -z "$cmd" || "$cmd" == "$pane_shell" ]]; then
break
fi
done