tomo docs
home github

The interface

On a terminal, tomo sync gives you a live view of the session — the calm stream you already know, a heartbeat that proves it's alive, and a conflict center a keypress away. Every screen has a plain, scriptable twin.

One model: a sync session is a server; every interface — the TUI, the plain stream, JSON, your scripts — is a client that attaches to it. UIs come and go over a local control socket; sync never blocks on a UI, and nothing the TUI does lacks a command-line equivalent.

The default surface #

When stdout is a real terminal, tomo sync runs the session in the background and attaches an interactive TUI. It's a thin client of the control channel — it renders event-stream data and issues commands over the socket, exactly as a script would.

Anything that isn't an interactive terminal falls back automatically to the classic line stream — byte-for-byte what earlier versions printed, so pipes, logs, and CI keep working unchanged:

ContextWhat you get
tomo sync on a terminalThe interactive TUI (this page).
tomo sync --plainThe classic line stream, no alternate screen.
tomo sync --jsonMachine-readable JSON event lines.
piped / redirected / not a ttyThe plain line stream, automatically.
TOMO_TUI=0Opt out of the TUI globally.

The TUI honors NO_COLOR, TOMO_COLOR, and TOMO_ASCII (plain glyphs instead of Unicode), and it degrades gracefully — a tiny terminal gets a minimal single-pane layout rather than a broken one.

The main screen #

The body is the stream — the same glyphs and wording as the plain output. The TUI adds exactly two things: a pinned transfer zone and a one-line status bar. No dashboard, no panes.

tomo sync — dev@laptop
connected vm8 (192.168.1.40) src/train.py 1.2 kB src/config.yaml 842 B conflict src/train.py — kept vm8's copy · c to review assets/logo.png 14 kB model.ckpt ██████████░░░░░░ 58% vm8 (192.168.1.40) ✓ connected · ⚠ 1 · last sync 2s ago · c conflicts · d detach · ? help

the stream, a pinned transfer zone, and the status line — nothing else

Main-screen keys #

A handful of keys, not fifteen:

KeyAction
cOpen the conflict center.
hOpen the history browser.
spacePause / resume syncing (both directions queue; the status line shouts ⏸ PAUSED until you resume).
/Filter the stream by path (Esc clears).
PgUp / PgDn / / Scroll the history; follow pauses while you're up.
End / GJump back to the live tail.
dDetach — leave the session running.
qStop the session (with a confirm) when you started it foreground; just detach when you attached to it.
?Toggle the help overlay.

The conflict center #

A Tomo conflict never blocks anything: the tree already converged deterministically and the losing version is safe in history. So this is a review flow, not an unblocking one — which is exactly why one-keystroke verdicts are safe to offer. Press c from the stream (the badge shows the count the moment a lands — no modal ever interrupts you).

tomo sync — conflict center
tomo ── vm8 ── connected ── ⚠ 3
CONFLICTS
> src/train.py 2m ago kept: vm8's copy src/config.yaml 2m ago kept: vm8's copy adoption from vm8 (12 files) ▸
src/train.py on disk now — vm8, 2s ago in history — you, 5s ago ────────────────────────── @@ -18,7 +18,9 @@ - lr = 3e-4 + lr = 1e-4 + warmup = 500
enter keep · t take yours · b keep both · space skip · a ack all · u undo · ? help = tomo conflicts resolve 7 --keep-current

list + live diff on the left/right, verdicts and the exact CLI command along the bottom

Conflict-center keys #

KeyActionCLI equivalent
j / k · / Move the selection.
EnterKeep current (acknowledge).conflicts resolve <id> --keep-current
tTake the version in history.conflicts resolve <id> --take-loser
bKeep both (writes .theirs).conflicts resolve <id> --both
spacePause / resume syncing (same as the main screen; j/k to move past a conflict you're not ready to decide).tomo pause / tomo resume
aAcknowledge all remaining (with a confirm).conflicts resolve --all --keep-current
uUndo the last verdict (kept something by mistake? it comes back unresolved; took the wrong side? the prior bytes restore from history).tomo restore + reopening the conflict
h / l · / Collapse / expand an adoption group.
c / EscBack to the stream.
Prefer the command line? Everything here is a one-shot too: tomo conflicts show <id-or-path> prints the same diff, and tomo conflicts resolve <id-or-path> --keep-current | --take-loser | --both applies a verdict. tomo conflicts resolve --interactive walks them one by one with a plain prompt. See the command reference.

The history browser #

Press h and every save tomo has ever captured is browsable without leaving the session. Pick a file (type to filter, Enter opens), and you get its timeline — newest first, each version diffed against the one before it, colored by which machine wrote it. m marks a version so you can compare any two directly. r restores the selected version — with a confirm — and the running session ships the restored bytes to the peer like any other edit. The footer always shows the equivalent one-shot: tomo restore <path> --version <n>.

KeyActionCLI equivalent
hOpen the picker (from the main screen).tomo log
j / kWalk the timeline; the diff pane follows.tomo diff
mMark a version; the pane compares marked ↔ selected.tomo diff <path> --version <a> --against <b>
rRestore the selected version (with a confirm).tomo restore <path> --version <n>
EscTimeline → picker → main screen.

Detach & attach #

Because the session is a server, the terminal you started it from is just one window onto it. Detach and it keeps syncing; re-attach from anywhere; run several attachments at once.

$ tomo sync -d dev@gpu-box:~/proj    # start detached, return to the shell
session started (pid 48120) — attach: tomo attach · stop: tomo stop

$ tomo attach                        # from any terminal — TUI by default
$ tomo attach --plain                # the line stream instead
$ tomo logs -f                       # tail the session log
$ tomo stop                          # clean shutdown

It's all scriptable #

Every session serves a local control socket at .tomo/state/ctl.sock (a unix-domain socket, inside .tomo/ so it's never synced). It carries a versioned event stream — connect/disconnect, synced/removed, conflict, transfer progress, heartbeat — and a command channel for every action the CLI exposes. The TUI is one client of it; your scripts are another.

$ tomo events --json | jq -r 'select(.event=="conflict") | .path'
src/train.py

The JSON records carry a "v":1 version and are additive-only from the moment they shipped — new fields may appear, none are removed or repurposed, and unknown fields are ignored on parse, so an older reader keeps working. The end-to-end scenarios assert on these shapes, so the contract is as firm as the --json guarantees on status, log, and conflicts.

The guarantee: every TUI action has a 1:1 command-line or control-channel equivalent — the TUI is never a privileged interface. That's what the conflict center's echo footer is quietly demonstrating every time you move the selection.