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
- Run the agent inside tmux.
- 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.
- A second pane, the follower, sits below it. It runs
tail -Fon the full log, so you also see every line of output, not only the board rows. - The agent blocks until the command is done.
- 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.
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 -fcommand 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.shruns 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.shis 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) getsjuneau_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 runsmvn, and it looks at the installed zip, the in-tree sources and the~/.m2copy 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 DIRworking directory,-n LINEShow many trailing pane lines to print,-t TITLEthe leader pane's title.-k fail|always|neverwhen to keep the panes open after the command ends (defaultfail, until Enter in the leader), and-K MINto close kept panes by themselves after MIN minutes.--reapstops anything the command left running;--keep-on-cancelkeeps 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_GRACEseconds 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 asJUNEAU_TERMINAL_CANCEL.listshows each pane (id, title, state, age);close --keptorclose PANEcloses finished panes with their followers;equalize PANEevens 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
tmux-pane.shopens 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).- 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 letscanceland the orphan report work from another shell. - The script waits for the leader, prints its last lines, and closes or keeps the panes per
-k. watch-run.shadds the juneau-terminal side: it chooses the log paths before anything opens, so nothing has to guess them, exportsJUNEAU_TERMINAL_FULL_LOG,JUNEAU_TERMINAL_BOARD_LOG,JUNEAU_TERMINAL_RESULTandJUNEAU_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 throughjuneau_terminal.py. - Hand the person the
tail -fline. 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) solistandcancelcan tell runs apart. - Never use
pkill -f push.pyto stop a run: it kills other people's runs and the next run of the same script. Usecancel. - 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.