Ovvi

herdr calls

Every herdr call and command the app uses, and the ones it never uses

API calls

CallUsed forPage
session.snapshotall state and herdr's version of an open session, read after events and after every write (each pane's agent_session gives its agent's session: Session titles, Relaunch with Chat); the health check every 30 s; waiting for a server the app startedReconcile
events.subscribe (long-lived)"read the snapshot again"Events
workspace.create | close | renamesidebar actions: new (page header +, space menu), close, rename in place; folders dropped on the Dock icon; Archived's Resume when no space holds the session's folder. The new space's root_pane gets the New pane chooserWrites
workspace.move_blockMove Up and Down, drag to reorder (always with the row's group: the space and its linked worktrees)Writes
tab.create | close | rename | movetab bar and sidebar actions: new (also from a space row; its root_pane gets the chooser), close, Close Other Tabs and Close Tabs to the Right, rename, Move Tab Left and Right, drag to reorder; move puts an open from a card or a link in a new tab right after its card's tab; Archived's Resume (a new tab after the space's selected tab)Writes
pane.split | close | movesplit (left and up add a swap), close, Replace Pane and the commands that change a pane's kind (a split, then a close of the old pane), a browser or plugin pane's create (a move beside the old pane, then a close), a plugin card's open to the side (a move beside the card), Archived's Resume to the Side (a split right of the keyboard card), ovvi agent beside a card (right or below), Move Pane to Tab ▸ (a tab, New Tab, New Space), drag (an edge of a card in the same tab is two moves and a swap; a tab dropped on another space or New Space moves pane by pane)Writes
pane.swapdrag onto a card in the same tab; the last step of a drop on a left or top edge; split left and up; with direction, ⌥⇧⌘ and an arrowWrites
pane.resizeResize Pane (View ▸ Pane, the palette), 5 % eachDividers
pane.renameRename Pane, Reset Pane Name; a browser card's typed address goes into its label; each move inside a plugin card (a click, Back, Forward) goes into its labelWrites, Browser
layout.applya browser or plugin pane: a new tab with one pane, its label and its placeholder command, never with a tab_id; also a link opened in a new tab, and a plugin card's open in a new tab or to the sideCreate, Popups, links and dialogs, Opening
pane.send_inputGo to Directory and Insert Path; the chooser's cd to its folder; a browser or plugin pane's placeholder line (The placeholder); an agent's launch line (or its bare command) from the chooser, Start Agent Here or Archived's Resume; Relaunch with Chat's quit command and its launch line; chat text that starts with /, with no ReturnWrites
pane.send_keysspecial keys and Ctrl chords typed in a terminal card; a chat's Stop (Esc, OpenCode Esc twice); Relaunch with Chat's Stop keys while the agent works or waits, then ⌃C; ⌃U before a placeholder lineTerminal plane
pane.readvisible: the hover card's lines for a pane that needs you; recent: find in a pane, and a Codex chat's last 8 lines every 0.5 s (Codex's screen check); visible again after a find scroll, for the highlightsFind bar
pane.process_infoclose and Replace Pane check, Go to Directory's shell check, program icons, the agent loader's failure check, a chat agent's launch marker in the foreground argv, a resumed Codex's session before its first prompt, Relaunch with Chat's wait for the shellWrites, Program icons
pane.getthe keyboard card's fresh folder for New Tab, Go to Directory and Insert Path, and for a browser or plugin pane's create; the new pane of a plugin card's open; the viewport's offset after a find scroll; the agent loader's ready check, every 250 ms (New pane); Relaunch with Chat's check that herdr let go of the agentWrites
agent.prompta chat's Send: {target, text}; herdr pastes it with bracketed paste and presses EnterSending and stopping
pane.report_metadatathe ovvi-shown mark on each pane in view, for ovvi status, pi-herd and pi's copy; the ovvi-chat mark on a chat pane once its launch is bound to its processes (Which panes have it)Agent requests
workspace.report_metadatathe ovvi-open mark on each space of a session a window has open, for ovvi statusAgent requests
pane.scrollfind in a pane: scroll herdr's viewport to a match, and back to the bottomFind bar
layout.set_split_ratiodivider drag, double-click, balance (⌥⌘=), equal splits as a pane opens or closes (Balance splits automatically), divider accessibilityLayout
layout.exportfallback when the leaf count is wrongRebuilding the tree

Two pane.report_metadata calls are not the app's: the ovvi command's ring (ovvi-req, Protocol), and the token ovvi-label after a process renames a browser pane by hand, which only makes herdr send the event that the rename does not (Agents open browser panes). An agent's ovvi open uses the app's own layout.apply, tab.move, pane.move and pane.rename, as a plugin card's open does (CLI). ovvi agent makes its shell pane with pane.split, or tab.create and tab.move.

Commands

herdr here is the machine's absolute path from its login shell (herdr on each machine).

herdr --session S terminal session control|observe <terminal_id> \
    --cols C --rows R [--takeover]
herdr --session S session list --json               # each machine: the Open Session panel, restore, "not running"; its socket_path once per session (a remote machine: per connection)
ssh -F /dev/null -S <ctl> -O forward -L <ctl>-<s8>:<socket_path> <target>   # a remote machine: herdr's socket, once per session and connection
herdr --session S session stop|delete S --json      # Open Session's Stop and Delete; errors as JSON on stderr
herdr --session S server        # remote: nohup, on request; This Mac: a launchd job (D47)

Not herdr. A remote machine's commands (the SSH master, the relay and its login shell) are in The SSH connection, and the file drop upload in Drag and drop. This Mac's:

$SHELL -lic '…; /usr/bin/env; …; command -v herdr || command -v "$HOME/.local/bin/herdr"'  # This Mac, at launch, at once after a run over 3 s, and at most every 30 s while herdr is not found: the login shell's environment and herdr's path
sh -c '…pipefail; exec 2>&1; curl -fsSL https://herdr.dev/install.sh | sh'  # Install herdr, on This Mac or over SSH
env -0                                              # This Mac: the user's keys for the server's launchd job
launchctl bootout gui/<uid>/<label>                 # This Mac: start a server (D47)
launchctl bootstrap gui/<uid> <plist>
sh -c 'g=$(command -v git) && { [ "$g" != /usr/bin/git ] || [ "$(uname)" != Darwin ] || xcode-select -p; }'   # once at launch: is git usable (xcode-select only for the /usr/bin/git stub)
git --no-optional-locks -C <root> status --porcelain=v2 --branch --untracked-files=no   # git state of a space row (D67): on start, when git's files change, every 60 s
git -C <root> rev-parse --absolute-git-dir --git-common-dir      # once per root, as its watch starts: git's files
perl -e '<lstat sweep>' 2 <HEAD> <refs> <packed-refs> <reftable> <config>   # every machine: one long-lived run per root while a window shows it (the sidebar) or a card watches it (`git.watch`)

The ovvi command's mailbox, on each machine (Protocol):

sh -c 'd=$HOME/.cache/ovvi; mkdir -p "$d/bin" "$d/rpc" && chmod 700 "$d/rpc" && find "$d/rpc" -type f -mmin +60 -exec rm -f {} + ; sed -n 2p "$d/bin/ovvi"'     # once on connect
sh -c 'f=$HOME/.cache/ovvi/bin/ovvi; base64 -d >"$f.$$" && chmod 755 "$f.$$" && mv "$f.$$" "$f" || rm -f "$f.$$"'     # the script, when missing or older
sh -c 'cd "$HOME/.cache/ovvi/rpc" && mv -- "$1.req" "$1.run" && head -c "$2" "$1.run" | base64' sh <id> 65537    # take a request
sh -c 'f=$HOME/.cache/ovvi/rpc/$1.in; [ ! -f "$f" ] || head -c "$2" "$f" | base64' sh <id> 1048577    # ovvi copy's input
sh -c 'cd "$HOME/.cache/ovvi/rpc" && base64 -d >"$1.o" && mv "$1.o" "$1.out" && printf "%s\n%s" "$2" "$3" >"$1.e" && mv "$1.e" "$1.exit"; s=$?; rm -f "$1.run" "$1.in"; exit $s' sh <id> <code> <err>    # the answer
sh -c '[ -e "$1" ]' sh <path>; sh -c '[ -d "$1" ]' sh <path>   # ovvi open and ovvi plugins: is the path there, is it a folder

A browser or plugin card's (Browser, State):

sh -c 'cat "$HOME/$1"' sh .local/state/ovvi/<S>/<browser or plugin id>/<iid>.json    # a remote machine: read the state file when the card is built (This Mac reads it in the app)
sh -c 'umask 077; f="$HOME/$1"; mkdir -p "${f%/*}" && cat >"$f.$$" && mv "$f.$$" "$f"' sh <path>   # a remote machine: write it, 500 ms after a navigation or a plugin's last change (This Mac writes it in the app)
sh -c 'rm -f "$HOME/$1"' sh <path>                  # a remote machine: a typed address, a close (This Mac removes it in the app)
find "$HOME/.local/state/ovvi/<S>" -type f -mtime +30 -delete       # once on connect
sh -c 'kill -WINCH "$@"' sh <pid>…                 # a placeholder redraws its new label
ssh -F /dev/null -S <ctl> -O forward -D 127.0.0.1:<P> <target>   # a remote machine: its first browser card's proxy
ssh -F /dev/null -S <ctl> -O cancel -D 127.0.0.1:<P> <target>    # its last card went

A plugin card's, on the pane's machine, with -z and NUL-separated names so a name can hold a line break (File Explorer and Git):

sh -c 'cd "$1" || exit; for f in * .[!.]* ..?*; do … printf "%s/\0" …; done; [ "$2" ] || exit 0; printf "\0"; for f in …; do printf "./%s\0" "$f"; done | git --no-optional-locks check-ignore -z --stdin' sh <folder> git   # a folder's entries (the File Explorer's tree), then the names git ignores
sh -c 'cd "$1"; if [ "$2" ]; then git ls-files -z -co --exclude-standard; else find . -name .git -prune -o -type f -print0; fi | head -c 8388608' sh <root> git   # every path, for the filter
sh -c '[ -f "$1" ] …; wc -c <"$1"; cksum <"$1" && base64 <"$1"' sh <path> 16777216   # read a file (size, hash, bytes)
sh -c 'if [ -d "$1" ]; then echo d; elif [ -e "$1" ]; then wc -c <"$1" && cksum <"$1"; …' sh <path>   # a file's size and hash, or a folder (files.info)
sh -c '…; mv -- "$1" "$2"' sh <path> <target>       # a rename that never replaces (files.move)
sh -c 't=$(mktemp …/.ovvi-save.XXXXXX); cp -p "$1" "$t"; base64 -d >"$t"; …; mv -f "$t" "$1"' sh <path> <hash> <size>    # save: the bytes in base64 on stdin, checked by hash and size
sh -c 'ls -1Ap "$1"; git status …; git ls-files -z -m -o … | xargs -0 cat; … | cksum' sh <path>   # a remote machine: the 2 s check while a card shows
git -C <folder> rev-parse --show-toplevel           # the root for File Explorer and Git
git --no-optional-locks -C <root> status --porcelain=v1 -z --untracked-files=all   # the tree's letters; the Git home's staged and unstaged changes
git --no-optional-locks -C <root> status --porcelain=v2 --branch …   # the Git home: the branch, ahead and behind
git -C <root> for-each-ref --format=%(HEAD)%(refname) refs/heads refs/remotes   # the Git home: the branch menu
git -C <root> log --branches --remotes --tags HEAD -n <500, 1000, …> --topo-order --no-show-signature --decorate=full --format=%H%x1f%P%x1f%an%x1f%at%x1f%D%x1f%s --   # the Git home's history of every branch
env GIT_TERMINAL_PROMPT=0 git -C <root> <write>     # the Git home's writes, one at a time (fetch, pull and push also from a space row's menu and File Explorer's status bar): fetch --all, pull, -c push.autoSetupRemote=true push, --literal-pathspecs add -A -- <path>, --literal-pathspecs reset -q -- <path>, add -A, reset -q, commit -m <message>, switch [--track] <branch>, init -q
env GIT_TERMINAL_PROMPT=0 git -C <root> --literal-pathspecs ls-files -z --others --exclude-standard -- <paths>, then restore --staged --worktree --source=HEAD -- <tracked>, clean -fq -- <untracked>   # discard changes (git.revert)
sh -c 'cd "$1"; h=$(git rev-parse -q --verify HEAD || git hash-object -t tree /dev/null); { git diff … "$h" --; git ls-files -z -o --exclude-standard | head -c 65536 | xargs -0 -r -n 1 git diff … --no-index -- /dev/null; } …' sh <root> …   # a diff: the working tree, with untracked files
git -C <root> show --no-color --no-ext-diff -M -m --first-parent --format= <hash>   # a diff: one commit
git -C <root> show -s --no-show-signature --format=%H%x1f%P%x1f%an%x1f%ae%x1f%at%x1f%s%x1f%b <hash> --   # a commit's page: its subject, body, author, date and parents
git -C <root> rev-parse --verify --quiet <ref>^{commit}   # the palette's Open Commit and `ovvi open git`: the hash a ref names

The palette, a terminal's links and the sidebar's Archived Sessions, on the page's machine (Opening links and files, Archived Sessions):

sh -c 'case $1 in "~"|"~/"*) p=$HOME${1#"~"} ;; /*) p=$1 ;; *) p=${2:-$HOME}/$1 ;; esac; if [ -d "$p" ] …' sh <path> <folder>   # a terminal link's path and the palette's Open File: what is there
sh -c '{ [ -z "$4" ] || …; <the agent's script>; } 2>/dev/null' sh 50 <offset> <perl> ""   # a page of an agent's closed sessions: perl over pi's and Claude Code's JSONL files, sqlite3 -readonly on Codex's and OpenCode's index
sh -c 'test -d "$1" && echo yes' sh <folder>        # Resume: is the session's folder there
sh -c 'cd -- "$2"; …; cd -- "$1" && pwd -P' sh <typed> <folder>   # the New pane chooser's folder: the folder a typed path names
sh -c 'exec "${SHELL:-sh}" -ilc "$1"' sh 'echo; for c in claude codex pi opencode; do command -v $c …; done; echo ovvi-end'   # the chooser: which agents the machine has

A plugin's ovvi.run runs its argv on the card's machine, inside the envelope. This Mac watches files and folders with FSEvents, with no command.

Plugin packages, on This Mac only (Packages and installs):

sh -c 'command -v "$1"' sh npm|pnpm|bun            # the package manager: the lockfile's, else the first found
npm pack <spec> --pack-destination <staging> --json --ignore-scripts   # an npm: source
tar -xzf <tarball> -C <staging>
env GIT_TERMINAL_PROMPT=0 git clone --quiet -- <url> <folder>          # a git: source
env GIT_TERMINAL_PROMPT=0 git -C <folder> checkout --quiet <ref> --
sh -c 'cd "$1" && shift && exec "$@" 2>&1' sh <package> npm ci --no-audit --no-fund --ignore-scripts --legacy-peer-deps   # its dependencies (`install` without a lockfile; pnpm, bun: their flags)
sh -c 'cd "$1" && shift && exec "$@" 2>&1' sh <package> npm run --silent build   # its build script: at install, Reload Plugin, Build

A chat's, on the pane's machine (Chat view):

sh -c '<prepare>' sh <token or file>                # before a Pi or OpenCode launch: write the extension or plugin, the password, the 30-day sweep
sh -c 'f=…; while [ ! -f "$f" ]; do sleep 0.5; done; …; awk "$4"… <fifo & exec tail -c +N -F "$f" >fifo' sh <path> <offset> <inode> <skip>   # follow a side file or a transcript, dropping the lines its parser skips
perl -e '<watch, 0.3 s>' .claude/sessions/<pid>.json   # watch Claude Code's session record, one long-lived run
sh -c 'test -f "$HOME/$1/ask"' sh .cache/ovvi/chat/T    # Claude Code: does the question's hook still wait
sh -c 'rm -f "$HOME/$1/ask"' sh .cache/ovvi/chat/T      # the question was answered in the terminal: its hook ends
sh -c 'd="$HOME/$1"; [ -f "$d/ask" ] && cat >"$d/answer.tmp" && mv "$d/answer.tmp" "$d/answer"' sh <folder>   # its answer
curl -sN -K - http://127.0.0.1:<port>/event           # OpenCode's events; the password from a here-document
curl -s -K - http://127.0.0.1:<port>/session/<id>/message   # its history; also GET /permission, /question, POST /…/<id>/reply

Any agent pane's, on its machine (Session titles):

sh -c '{ <script>; } 2>/dev/null' sh <session>      # its session's title, when the session, the terminal title or the status changes

Tests only

The UI tests call these on throwaway panes; the app never does: pane.report_agent and pane.release_agent (agent status), pane.send_text (type into a pane) and pane.zoom (a tab zoomed as a herdr TUI zooms it).

Never used

pane.focus, tab.focus, workspace.focus, agent.focus, herdr popups, herdr plugin panes (plugin.pane.open), herdr's private client socket.

On this page