#!/usr/bin/env bash # Open one screen as a TUIOS pane, or adopt the pane that already runs it. # # One screen, one workspace, named after the screen. The pane runs a shell that # starts the program and returns to the shell when the program exits, so the # pane never closes by itself; a workspace that loses its last pane is deleted # by TUIOS and the rest renumber, which is the failure this avoids. # # bin/screen tree # bin/screen reply AUD-162 # # Run it twice and the second run focuses the existing pane. Print "created" or # "adopted" so the caller can tell which happened. set -euo pipefail # The session is named, never taken from where the caller happens to be. # TUIOS_SESSION is the session of the pane you are typing in, which is not # necessarily this project's session; using it once put a screen in the wrong # place. SESSION="${PAPERCLIP_HARNESS_SESSION:-paperclip-harness}" ROOT="$(cd "$(dirname "$0")/.." && pwd)" BIN="$ROOT/bin/harness" screen="${1:-}" case "$screen" in tree) args=(-screen tree -project "${2:-spike-2026-11-10}") ;; new) args=(-screen new -project "${2:-spike-2026-11-10}") ;; log) args=(-screen log -issue "${2:?log needs a task, for example AUD-160}") ;; reply) args=(-screen reply -issue "${2:?reply needs a task, for example AUD-162}") ;; *) echo "usage: $(basename "$0") tree|new [project] | reply|log " >&2; exit 2 ;; esac if [ ! -x "$BIN" ] || [ -n "$(find "$ROOT/tui" -name '*.go' -newer "$BIN" -print -quit 2>/dev/null)" ]; then if ! command -v go >/dev/null 2>&1; then echo "no harness binary and no Go to build one" >&2 exit 1 fi ( cd "$ROOT/tui" && go build -o "$BIN" . ) fi # Workspaces and windows are resolved by name every time. A number is never # remembered, because closing a pane renumbers the rest. state="$(python3 - "$SESSION" "$screen" <<'PY' import json, subprocess, sys session, want = sys.argv[1], sys.argv[2] def call(*args): out = subprocess.run(["tuios", *args, "--json"], capture_output=True, text=True) if out.returncode != 0: sys.exit("tuios " + " ".join(args) + " failed: " + out.stderr.strip()) return json.loads(out.stdout) ws = call("list-workspaces", "-s", session)["workspaces"] win = call("list-windows", "-s", session)["windows"] for w in win: # custom_name is the name we gave the pane. It survives the program # exiting and a shell taking over the title, so it is what adoption reads. if w.get("custom_name") == want: print("adopt", w["window_id"]); sys.exit() named = next((w["workspace"] for w in ws if w.get("name") == want), None) if named is not None: print("create", named); sys.exit() free = next((w["workspace"] for w in ws if w.get("window_count") == 0), None) if free is None: sys.exit("no empty workspace in session %r; close a pane or add a workspace" % session) print("create", free) PY )" action="$(echo "$state" | awk '{print $1}')" where="$(echo "$state" | awk '{print $2}')" if [ "$action" = "adopt" ]; then tuios focus-window "$where" -s "$SESSION" >/dev/null echo "adopted $screen ($where) in session $SESSION" exit 0 fi tuios new-window "$screen" -s "$SESSION" --workspace "$where" --cwd "$ROOT" \ --no-focus --keep-open -- /bin/sh -c "$BIN ${args[*]}; exec zsh -i" >/dev/null tuios set-workspace-name "$where" "$screen" -s "$SESSION" >/dev/null echo "created $screen on workspace $where of session $SESSION"