Skip to main content

Watching a Run from an AI Agent

An AI coding agent such as Claude Code runs a shell command and shows its output when the command finishes. A six-minute push.py or Maven gate is therefore a silent wait: no progress, no sign of which module is building. This page shows how to give the run a live pane beside the chat, using tmux and juneau-terminal's board view.

The approach​

  1. Run the agent inside tmux.
  2. Have the agent start the long command in a side pane. The pane has a real terminal, so juneau-terminal draws its live view there, and focus stays where you are typing. This is the leader pane.
  3. A second pane, the follower, sits below it. It runs tail -F on the full log, so you also see every line of output, not only the board rows.
  4. The agent blocks until the command is done.
  5. It reads the result back from the pane's last lines and the command's exit code, and carries on.

You watch the run as it happens. The agent still gets what it needs: the final rows and a pass or fail.

An agent pane beside a leader pane and a follower pane

What juneau-terminal contributes​

With JUNEAU_TERMINAL_CONSOLE=board (or --console board), a run in the leader pane shows:

  • one row per step, with its time, a progress bar, counts and a line of summary;
  • the module bar for a Maven build, and a dot line under each running module, test file or check;
  • a header that prints the tail -f command for the full log.

By default the full log is written under ~/.cache/juneau-terminal/ (the newest 20 are kept), so the agent can read the details of a failure without scrolling the pane. A run also accepts the paths up front, in JUNEAU_TERMINAL_FULL_LOG and JUNEAU_TERMINAL_BOARD_LOG; watch-run.sh uses that. See Console view for the options.

The scripts​

The toolkit zip, org.apache.juneau:juneau-terminal:<version>:zip, carries two bash scripts in juneau_terminal_scripts/bin/:

  • tmux-pane.sh runs a command in a side pane, blocks until it ends and exits with its code. It also cancels, lists, closes and equalizes such panes. It knows nothing about juneau-terminal.
  • watch-run.sh is a wrapper for a juneau-terminal script (push.py, release.py, deploy*.py). It picks the log and result paths, opens the follower pane, and prints a result summary when the script ends.

They need bash, tmux, ps, pgrep and perl; python3 (for the summary) and lsof (for the listening ports in the orphan report) are optional. They are tested on macOS.

Getting them​

A repository gets them with its copy of the toolkit; there is nothing separate to install.

That is the interim route until Juneau 10.0.0. The plan for the 10.0 release is a juneau-terminal command line from PyPI or Homebrew, with watch and pane subcommands for these scripts; see the note on juneau-terminal.

  • A repository that vendors the toolkit (a tree pinned to a manifest-checked copy of juneau-terminal) gets juneau_terminal_scripts/bin/ on its next refresh of that copy.
  • A repository with scripts/bootstrap.py (the loader every push script vendors) asks it for the path with --bin. It never runs mvn, and it looks at the installed zip, the in-tree sources and the ~/.m2 copy for the pom's <juneau.version>.
  • The Juneau repository has the scripts in juneau-terminal/src/main/python/juneau_terminal_scripts/bin/, and runs them in place.

Locate a script, check it, then use it:

wr=$(python3 scripts/bootstrap.py --bin watch-run.sh) && [ -x "$wr" ] && "$wr" -d . -t push -- python3 scripts/push.py "message"

Without a vendored bootstrap.py, the installed zip is itself runnable:

J=~/.m2/repository/org/apache/juneau/juneau-terminal/<v>/juneau-terminal-<v>.zip
wr=$(python3 "$J" --bin watch-run.sh) && [ -x "$wr" ] && echo "$wr"

When the source is a zip, the scripts are extracted to ~/.cache/juneau-terminal/bin/<version>/, outside target/ so that mvn clean never deletes the cancel path of a run still going. Each file is renamed into place, so a running script keeps the file it opened.

Using them​

"$wr" -d ~/git/some/repo -t "push" -- python3 scripts/push.py "message"
"$wr" --no-tail -- python3 scripts/release.py

watch-run.sh prints tail -f <full log> and board: <board log> first; hand the tail -f line to the person watching, since it works from any terminal. When the script ends it prints the pane's last lines, a result: summary and exit=N, so read the last exit= line. It also closes finished panes left by earlier runs before opening a new one; a pane that is still running is left alone.

Options (both scripts take the first group; watch-run.sh passes them to tmux-pane.sh):

  • -d DIR working directory, -n LINES how many trailing pane lines to print, -t TITLE the leader pane's title.
  • -k fail|always|never when to keep the panes open after the command ends (default fail, until Enter in the leader), and -K MIN to close kept panes by themselves after MIN minutes.
  • --reap stops anything the command left running; --keep-on-cancel keeps a cancelled run's panes per -k.
  • --no-tail (watch-run.sh) leaves out the follower. --pane 'CMD' (tmux-pane.sh, repeatable, with --size N% before a follower) runs several panes; the first is the leader.

Subcommands of tmux-pane.sh:

  • cancel [PANE|TITLE|--all] stops a run from any shell. It sends INT, then TERM, then KILL to the command's process group, TMUX_PANE_GRACE seconds apart (default 10), then stops what is left by PID and start time, never by name. The command that does it is exported to the run as JUNEAU_TERMINAL_CANCEL.
  • list shows each pane (id, title, state, age); close --kept or close PANE closes finished panes with their followers; equalize PANE evens out the column widths.

The leader's exit code is the script's. A usage error, or running outside tmux, exits 2; a leader pane closed before the command ended exits 129. Whenever a run ends, processes it started that are still running are listed as orphans, with their listening ports.

Equal widths​

The agent pane counts as one column, and each run's leader with its followers is another, so one run gives a 50/50 split and two runs give thirds, and so on. The columns are equalized when a leader opens and again when a run's panes close. The agent pane is remembered in the @tmux_agent pane option. Other panes and other windows are not touched, and a failed resize only prints a warning.

The follower filter​

The follower shows the log through a filter; the log file itself is unchanged. U+2028, U+2029 and other format characters (\p{Cf}) become a space, because tmux and the outer terminal disagree on their width and a line's end then overflows into the pane to the left. Control characters other than tab, newline and ESC (so colour survives), and DEL, are dropped. Invalid UTF-8 becomes U+FFFD. watch-run.sh --print-tail-filter prints the filter.

Without tmux​

With $TMUX unset both scripts exit 2. Run the command directly and follow its log from another terminal:

JUNEAU_TERMINAL_CONSOLE=board python3 push.py > run.log 2>&1 &
tail -f run.log

How it works​

  1. tmux-pane.sh opens the leader pane to the right of the pane it was started from, without taking focus, and gives every pane its environment (a pane otherwise starts with the tmux server's).
  2. The command runs as its own process group. While it runs, the script records its descendants about every two seconds, by PID and start time, in a registry under ${TMPDIR:-/tmp}/tmux-pane/; that registry is what lets cancel and the orphan report work from another shell.
  3. The script waits for the leader, prints its last lines, and closes or keeps the panes per -k.
  4. watch-run.sh adds the juneau-terminal side: it chooses the log paths before anything opens, so nothing has to guess them, exports JUNEAU_TERMINAL_FULL_LOG, JUNEAU_TERMINAL_BOARD_LOG, JUNEAU_TERMINAL_RESULT and JUNEAU_TERMINAL_CANCEL, and reads the result file when the command ends.

Each script's header comment is its full usage text.

Tips​

  • The panes don't take focus, so typing in the chat is not interrupted.
  • It works for any command juneau-terminal wraps: test.py, push.py, release.py, or a Maven, pytest or Playwright run started through juneau_terminal.py.
  • Hand the person the tail -f line. It works from any terminal and shows the same full log as the follower.
  • Close the panes with Enter in the leader when you have seen enough; the agent already has its output.
  • Give each run its own title (-t) so list and cancel can tell runs apart.
  • Never use pkill -f push.py to stop a run: it kills other people's runs and the next run of the same script. Use cancel.
  • Edit a script while a run is using it by writing a temporary file and mv-ing it over the original. A running Bash reads its script as it goes, so editing in place can break a run that is in progress.

A tool that needs structured progress and a machine-readable outcome, rather than the pane's text, can follow a markers file and read a result file; see Driving a Run from Another Tool.

See also​