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.
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:
| Context | What you get |
|---|---|
tomo sync on a terminal | The interactive TUI (this page). |
tomo sync --plain | The classic line stream, no alternate screen. |
tomo sync --json | Machine-readable JSON event lines. |
| piped / redirected / not a tty | The plain line stream, automatically. |
TOMO_TUI=0 | Opt 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.
the stream, a pinned transfer zone, and the status line — nothing else
- Status line. Peer name and connection state (a reconnect countdown when offline), a ⚠ conflict badge, and a last sync Ns ago heartbeat — the thing that makes a quiet screen legible, since "working" and "dead" look identical without it. A paused session says so loudly here.
- Pinned transfer zone. The transient progress the plain stream already draws, held at a stable spot above the status line. Concurrent transfers stack; it takes zero height when idle.
- Scrollback. PgUp browses history without losing tail-follow; new events raise a
▾ new activitynudge, and End / G re-sticks to the live tail. - Filter. / narrows the stream to matching paths, less-style; Esc clears it. The active filter shows in the status line.
Main-screen keys #
A handful of keys, not fifteen:
| Key | Action |
|---|---|
| c | Open the conflict center. |
| h | Open the history browser. |
| space | Pause / 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 / G | Jump back to the live tail. |
| d | Detach — leave the session running. |
| q | Stop 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).
list + live diff on the left/right, verdicts and the exact CLI command along the bottom
- Semantic framing, never A/B. The winner is "on disk now — vm8's copy"; the loser is "in history — yours". Your side stays cyan, the peer magenta, everywhere in the TUI. (Timestamps are display-only wall time; ordering still uses vector clocks.)
- Single-key verdicts. Enter keeps the current file (acknowledge — the common case), t takes the version in history (restores it and syncs it out live), b keeps both (materializes
<path>.theirsto merge by hand). Selection auto-advances Gmail-style; the last one resolved drops you back to the stream. - Undo. u flips the last verdict — a resolution is itself reversible, because every version is in history. This is the property git conflict UX can't offer.
- Adoption groups. A first-contact sync arrives as a collapsible
adoption from vm8 (12 files) ▸row. A verdict on the header applies to the whole group; expand with l to cherry-pick. - The CLI echo. The bottom line always shows the exact command for the highlighted action (
= tomo conflicts resolve 7 --keep-current) — passive use of the TUI teaches you its scriptable twin and proves the TUI holds no privileged powers.
Conflict-center keys #
| Key | Action | CLI equivalent |
|---|---|---|
| j / k · ↓ / ↑ | Move the selection. | — |
| Enter | Keep current (acknowledge). | conflicts resolve <id> --keep-current |
| t | Take the version in history. | conflicts resolve <id> --take-loser |
| b | Keep both (writes .theirs). | conflicts resolve <id> --both |
| space | Pause / resume syncing (same as the main screen; j/k to move past a conflict you're not ready to decide). | tomo pause / tomo resume |
| a | Acknowledge all remaining (with a confirm). | conflicts resolve --all --keep-current |
| u | Undo 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 / Esc | Back to the stream. | — |
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>.
| Key | Action | CLI equivalent |
|---|---|---|
| h | Open the picker (from the main screen). | tomo log |
| j / k | Walk the timeline; the diff pane follows. | tomo diff |
| m | Mark a version; the pane compares marked ↔ selected. | tomo diff <path> --version <a> --against <b> |
| r | Restore the selected version (with a confirm). | tomo restore <path> --version <n> |
| Esc | Timeline → 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
- Foreground is still the default. A bare
tomo syncruns the session and attaches in one step — it's exactlysync -dplus an implicitattach, so there's one session codepath, not two. - The session survives the terminal. Closing an attached terminal detaches; it never kills the session unless you started it foreground and it takes your Ctrl-C.
- Multiple clients, all equal. Every attached client can issue commands (the session serializes them, same as two terminals running CLI commands). None is privileged.
- Exit summary. The TUI runs on the alternate screen, so on exit it prints a compact recap —
synced 214 files · 2 conflicts resolved · 1 open · 47 min— instead of leaving empty scrollback.
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.