Answer your AI coding agents from your phone.
herdrup is an iOS client for herdr — a calm status board for the coding agents running on your machine, with a real terminal one tap behind each. When an agent needs you, you see it at a glance and can reply from anywhere.
⬇ Download on the App Store · 🌐 herdrup.themartian.app
The framing is a herdr client that contains a terminal, not a terminal app that happens to run herdr. A generic SSH terminal renders a character grid and cannot know what a pane or an agent is; herdr's JSON API exposes both, so panes and agents become real UI objects instead of pixels.
- Status board — every agent grouped by what it needs: needs you (amber), working, done,
stopped. Colour is meaning, not decoration — the one signal that matters reads instantly.
On a herdr that advertises
events_v2, rows change as status events arrive, including agents on federated machines the coordinator relays; the list is re-fetched every 30 s as a backstop and after a stream reconnect. An older herdr is polled every 5 s. - Live terminal — a full SwiftTerm terminal for any pane, one tap behind its card, with gestures to page between agents, tail the output, and scroll history. Resizing preserves the logical history position; live followers stay at the tail. The on-screen Ctrl key arms one native terminal chord in direct input, or a control character in the reply field.
- Gram — direct messaging between you and your agents: get pinged when one needs input, send text, and share images, videos, or files (several at once) straight to an agent.
- Composer: Terminal and Gram share one editor. It is a single row beside the mic and send buttons until the text wraps; then the buttons drop to a toolbar and the text grows to five lines before scrolling. From three lines, a handle opens a tall editor. Dictation shows a live waveform and a glowing border. Attachments show per-file progress inside the card. Terminal quick keys stay above the card. In a terminal the composer grows over the terminal's bottom rows instead of shrinking it, so typing never resizes the agent's screen.
- Guest access — share one agent with someone outside your machines from its ••• menu. They
get a one-use invite (QR code or link), watch the live terminal and message the agent through
a relay, end to end encrypted, with every message labelled
<name> (via HerdrUp):. Settings → Shared access lists who has access on each machine, revokes it, and shows the activity log.
herdrup is a client — it talks to a herdr daemon running on your own machine over SSH (nothing is public; it rides your own network). You need the fork that adds the gram / push / live-terminal APIs: jerryfane/herdr. The app tells you if it's talking to a daemon that doesn't have them.
Push notifications go through the HerdrUp push relay (push.herdrup.themartian.app), so your machine
needs no Apple push key. The app enrolls its push tokens with the relay and gives the daemon the sealed
capability it gets back; notification text passes through the relay but is never stored or logged.
Settings → Notifications shows whether the connected machine can send them.
The SSH transport is pure Swift (swift-nio-ssh + Citadel) — there is no system libssh2 to install, which is exactly what lets the protocol layer link into iOS.
Download on the App Store — herdrup is live, free, and iPhone + iPad (it also installs on Apple Silicon Macs as a Designed-for-iPad app).
You can also build it yourself; it is Apache-2.0 and the whole client is in this repository.
The Xcode project is generated from project.yml with
XcodeGen (there is no committed .xcodeproj), so the app can be
authored and reviewed without an opaque project blob.
# The app (macOS + Xcode)
brew install xcodegen
xcodegen generate
xcodebuild -scheme Herdr -destination 'generic/platform=iOS Simulator' build
# The protocol layer, HerdrKit — builds and tests on Linux and macOS
swift build
swift test # live tests run when a herdr socket + sshd are present; skip otherwise
# The vendored terminal core — also Linux-buildable
swift test --package-path Vendor/SwiftTermHerdrKit contains no UIKit or SwiftUI, so it stays Linux-buildable and can be exercised against a real
herdr server; the iOS app consumes it unchanged. Floors: macOS 14+, iOS 17+ (declared in Package.swift
— macOS 14 because Citadel requires it).
SwiftTerm is vendored at Vendor/SwiftTerm, based on upstream v1.15.0,
commit dd2fb8ac5b861e7bf617c872895e338f38165648. Local changes retain a logical viewport cell
through reflow, keep terminal modes during geometry/font commits, and expose managed-size and
completed-paint hooks. The app commits geometry only from ordered stream frames; a short retained
frame covers resize transitions without stretching text or restarting the stream. For unmarked output,
reveal uses a bounded quiet/deadline heuristic rather than assuming a semantic redraw-complete signal.
CI runs core tests on Linux and UIKit/interaction regressions on both iPhone and iPad simulators.
App/ the SwiftUI app (terminal, status board, Gram, Settings) — iOS only
Sources/HerdrKit/ pure-Swift transport + typed API — Linux-testable, no UI framework
CitadelTransport.swift pure-Swift SSH transport: execs `herdr api-bridge` per channel
HerdrClient.swift typed API: agentList, read, prompt, sendKeys, gram, subscribe
AgentStatusStream.swift all-pane status stream lines (events v2) and row patching
AgentList.swift agent-list model with fail-open-visible unknown statuses
HostKeyPinning.swift TOFU host-key policy + the nio-ssh validator bridge
SessionRecovery.swift reconnect/resync policy: attempt identity, the subscription ledger
Tests/HerdrKitTests/ protocol/transport unit tests (run on Linux)
A guide to the main files, not an exhaustive inventory — swift package describe is authoritative.
These were established against a running server, not read off the source, and they shape the transport:
| fact | consequence |
|---|---|
| The command socket is single-shot — one request per connection | every command needs its own SSH channel |
events.subscribe is persistent |
one long-lived event channel + N short-lived request channels |
| Subscriptions are pane-scoped, no wildcard, before events v2 | watching N panes means N entries + re-subscribe on pane creation; an events_v2 daemon accepts all-pane status and turn entries, so the status board holds one stream |
agent.read --format ansi returns real styling |
faithful rendering needs no new transport |
agent.list carries revision + state_change_seq |
refresh can be revision-gated instead of blind |
Dark, deep-desaturated navy — never black. The design system (App/DesignSystem.swift) is taken from the
Claude Design kit (design: jerryfane/herdr#28): colour is
meaning (amber = waiting on you, red = died, blue = working, green = done), monospace is the machine
voice and a proportional sans is the app voice.