remobi-setup
Interactive onboarding skill for
remobi — monitor and control tmux from your phone.
This skill walks the user through the full setup journey in one conversation. Each phase builds on the last; skip phases the user doesn't need.
Workflow
Phase 1: Assess environment
Check what's installed and help fill gaps.
bash
node --version # need >= 22
which ttyd # must be on PATH
tmux -V # target multiplexer
which remobi # npm install -g remobi
If anything is missing, help install it:
- Node: suggest mise, nvm, or direct install
- ttyd: (macOS), distro package or source build (Linux) — see ttyd installation
- tmux: or distro package
- remobi:
Move on once all four are present.
Phase 2: Inspect tmux setup
Gather the user's tmux configuration to inform config generation.
bash
tmux show-options -g prefix # prefix key
tmux list-keys # all bindings
tmux show-options -g mouse # mouse mode
tmux show-options -g status-left # status bar
tmux list-keys | grep display-popup # popup bindings
If tmux isn't running, fall back to reading the config file directly:
bash
cat ~/.config/tmux/tmux.conf 2>/dev/null || cat ~/.tmux.conf 2>/dev/null
Note down:
- Prefix key and byte (Ctrl-B = , Ctrl-A = , etc.)
- Custom bindings worth surfacing as buttons (especially popup bindings for lazygit, yazi, neovim, fzf pickers, gh-dash, scratch shells)
- Whether mouse mode is on
- Status bar complexity (affects mobile width recommendations)
- Plugin manager (tpm, etc.)
If the user has no tmux config at all, offer to help set up a basic one before continuing.
Phase 3: Interview the user
Ask questions one at a time — don't dump a list. Adapt based on what you learned in phase 2.
- What do you primarily use tmux for? (coding agents, dev workflow, server monitoring, all of the above)
- Do you use popup bindings for tools? Which ones? (lazygit, yazi, neovim, scratch shell, gh-dash, session picker)
- Do you want touch scrolling? What strategy? ( for mouse-event scrolling, for PageUp/PageDown paging)
- Auto-zoom on mobile? When you open remobi on your phone, should the current pane zoom to full screen automatically?
- Floating zoom button? A persistent button overlaid on the terminal for one-tap zoom toggle
- Custom theme or Catppuccin Mocha? (Catppuccin Mocha is the default and looks great — only ask if the user's tmux theme is clearly different)
- Font preference? (default: JetBrainsMono NFM)
- Any other tmux bindings you want on your phone? (This catches anything the inspection missed)
Skip questions where you already know the answer from phase 2. Summarise what you've gathered before moving to config generation.
Phase 4: Generate
Write the config using
. Only include keys that differ from defaults — omit everything else.
typescript
import { defineConfig } from 'remobi'
export default defineConfig({
// Only non-default overrides here
})
After writing, validate:
A zero exit with "Dry run: build" output means valid. Fix any errors and re-validate until clean.
See
Config reference below for the full schema, allowed keys, action types, and escape codes.
Phase 5: Suggest tmux mobile optimisations
Ask: "Would you like suggestions for making your tmux config more mobile-friendly?"
If yes, run the checks below. If tmux isn't running, read the config file directly. For full context and examples, read
references/mobile-tmux.md
and
references/mobile-panes.md
.
| Check | Command | Good sign | Suggestion if missing |
|---|
| Responsive status-left | | Contains | Add width breakpoints to strip content on narrow terminals |
| Responsive status-right | tmux show -g status-right
| Contains or calls a script | Progressive content stripping |
| Popup sizing | tmux list-keys | grep display-popup
| Uses dimensions | Replace fixed char sizes with / |
| Zoom indicator | | Contains | Add #{?window_zoomed_flag,[Z] ,}
|
| Mouse mode | | | |
| Window renumbering | tmux show -g renumber-windows
| | set -g renumber-windows on
|
| Zoom-aware navigation | tmux list-keys | grep 'select-pane.*resize-pane'
| Present | Add zoom-aware / bindings (see references/mobile-panes.md
) |
For each missing item, offer a concrete snippet the user can paste into
. Suggest snippets only — never modify
without explicit permission.
Phase 6: Deployment guidance
Ask: "How do you want to access remobi from your phone?"
remobi is a remote-control surface for your terminal — never expose it to the public internet. All deployment options below keep access private.
Common options:
- Tailscale Serve (recommended) — HTTPS over your private tailnet. Read
references/tailscale-serve.md
for the full guide.
- Cloudflare Tunnel + Access — private tunnel with Cloudflare Access policies controlling who can connect (e.g. restrict by email, IdP group, device posture). Do not use unauthenticated quick tunnels.
- Local network only — on localhost behind your own VPN or private network.
For macOS users, mention
and point to
for persistent options.
For users who want manual ttyd control, point to
.
Phase 7: Summarise
Tell the user:
- What was configured and why (prefix byte, custom bindings, gestures, theme)
- How to start:
- How to access from their phone (URL from deployment choice)
- PWA install: on mobile, tap "Add to Home Screen" for a standalone app experience
Config reference
Allowed root keys
Exactly these — validation rejects anything else:
name theme font toolbar drawer gestures mobile floatingButtons pwa reconnect
ButtonAction union
| Required fields | Notes |
|---|
| | Optional for help overlay |
| (none) | Opens Ctrl+key combo UI |
| (none) | Paste from clipboard |
| (none) | Opens Ctrl/Alt + key modal |
| (none) | Opens/closes command drawer |
Non-
actions must NOT have
or
— the validator rejects them.
ControlButton shape
Every button in toolbar rows, drawer, and floatingButtons uses this schema:
typescript
{
id: string // unique within its array
label: string // text shown on the button
description: string // shown in help overlay — keep user-facing and clear
action: ButtonAction
}
Button array forms (, , )
Two forms — pick the least invasive:
typescript
// 1. Replace entirely (plain array)
toolbar: { row1: [{ id, label, description, action }, ...] }
// 2. Transform (function receives defaults, returns new array)
toolbar: { row2: (defaults) => defaults.filter(b => b.id !== 'q') }
// Function form covers all operations via standard JS:
// - Append: (d) => [...d, newBtn]
// - Prepend: (d) => [newBtn, ...d]
// - Remove: (d) => d.filter(b => b.id !== 'q')
// - Replace: (d) => d.map(b => b.id === 'tmux-prefix' ? newBtn : b)
// - Insert: (d) => { const i = d.findIndex(b => b.id === 'tab'); return [...d.slice(0,i), newBtn, ...d.slice(i)] }
Floating buttons
Must use the grouped shape — a flat
is rejected:
typescript
floatingButtons: [
{
position: 'top-left', // required
direction: 'row', // optional: 'row' | 'column' (default 'row')
buttons: [{ id, label, description, action }],
},
]
Valid positions:
top-left | top-right | top-centre | bottom-left | bottom-right | bottom-centre | centre-left | centre-right
Escape-code cheat sheet
Use these in
and gesture
/
fields:
| Key | Escape sequence | Notes |
|---|
| Ctrl-B (prefix) | | Default tmux prefix |
| Ctrl-A (prefix) | | screen/byobu/custom prefix |
| Ctrl-C | | Interrupt |
| Ctrl-D | | EOF / exit shell |
| Escape | | |
| Tab | | |
| Shift+Tab | | |
| Enter | | |
| Alt+Enter | | |
| Backspace | | DEL character |
| Up arrow | | |
| Down arrow | | |
| Right arrow | | |
| Left arrow | | |
| Page Up | | |
| Page Down | | |
| Space | | literal space |
Composing tmux key sequences
tmux bindings are
+
. Concatenate the bytes:
Ctrl-B + c → '\x02c' (new window)
Ctrl-B + n → '\x02n' (next window)
Ctrl-B + p → '\x02p' (previous window)
Ctrl-B + z → '\x02z' (zoom pane)
Ctrl-B + % → '\x02%' (split vertical — stock tmux)
Ctrl-B + " → '\x02"' (split horizontal — stock tmux)
Ctrl-B + [ → '\x02[' (copy mode)
Ctrl-B + d → '\x02d' (detach)
For a custom prefix (e.g. Ctrl-A): replace
with
.
Example configs
Minimal — default Ctrl-B prefix, custom name only
typescript
import { defineConfig } from 'remobi'
export default defineConfig({
name: 'dev',
})
Custom prefix — Ctrl-A (screen/byobu style)
Replace the default
button and update swipe gestures:
typescript
import { defineConfig } from 'remobi'
export default defineConfig({
name: 'dev',
toolbar: {
row1: (defaults) => defaults.map(b =>
b.id === 'tmux-prefix'
? { ...b, description: 'Send tmux prefix key (Ctrl-A)', action: { type: 'send', data: '\x01' } }
: b
),
},
gestures: {
swipe: {
left: '\x01n',
right: '\x01p',
leftLabel: 'Next tmux window',
rightLabel: 'Previous tmux window',
},
},
drawer: {
buttons: (defaults) => defaults.map(b => {
// Remap tmux-prefixed buttons from Ctrl-B (\x02) to Ctrl-A (\x01)
if (b.action.type === 'send' && b.action.data.startsWith('\x02')) {
return { ...b, action: { ...b.action, data: '\x01' + b.action.data.slice(1) } }
}
return b
}),
},
})
Floating buttons + mobile auto-zoom
typescript
import { defineConfig } from 'remobi'
export default defineConfig({
mobile: {
initData: '\x02z', // zoom focused pane on mobile load
widthThreshold: 768,
},
floatingButtons: [
{
position: 'top-left',
buttons: [
{
id: 'zoom',
label: 'Zoom',
description: 'Toggle pane zoom',
action: { type: 'send', data: '\x02z' },
},
],
},
],
})
Guardrails
- Never invent root keys. The validator rejects unknown keys with a path-based error.
- Use , never — the latter was renamed and no longer works.
- actions require — omitting it fails validation.
- Non- actions must not have or — validator rejects them.
- is an array of groups — wrap buttons in .
- has and — there is no or flat key on toolbar.
- is — set to to disable, not or .
- has only — defaults to . Set to disable.
Validation
bash
remobi build --dry-run # validates config, prints plan, exits without building
remobi build --dry-run -c ./remobi.config.ts # explicit path
A zero exit with "Dry run: build" output means the config is valid. Any error output means fix the reported paths before proceeding.
Common validation errors
| Error | Cause | Fix |
|---|
| Invented or legacy root key | Remove it; only allowed root keys are valid |
| Old key name | Rename to |
| Wrong toolbar shape | Use and/or |
action.type: expected 'send' | ...
| Wrong type string | Use exact literal from ButtonAction union |
action.data: expected string, received undefined
| action missing | Add |
action.data: expected undefined
| on non- action | Remove from non- actions |
floatingButtons[0]: expected object
| Flat | Wrap in group: { position: 'top-left', buttons: [...] }
|
mobile.initData: expected string or null
| or passed | Use to disable, or a string to send |