swimmers 0.1.0

Axum server plus TUI for orchestrating Claude Code and Codex agents across tmux panes
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# swimmers

<div align="center">

```
   o   .          o O  .        z z
><o)))'>        ><O)))'>          ><-)))'>
  /_/_            /_/_              \_\
      .             O   o
  active            busy           sleeping
```

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/build000r/swimmers/blob/main/LICENSE)
[![Rust](https://img.shields.io/badge/Rust-2021-orange.svg)](https://www.rust-lang.org/)

**Quick Start**

```bash
cargo install swimmers
swimmers &          # start the API server on 127.0.0.1:3210
swimmers-tui        # open the aquarium TUI
```

</div>

A terminal aquarium for your tmux sessions. Each session becomes an animated fish whose behavior reflects its real-time state — swimming when active, bubbling when busy, dozing when idle. Backed by a Rust API server that discovers and manages tmux sessions, with a native TUI client that renders the whole thing as a fish bowl you can navigate, inspect, and control.

---

## TL;DR

**The Problem**: You have a dozen tmux sessions running across a machine. Listing them with `tmux ls` gives you cryptic one-liners. You can't tell at a glance which sessions are busy, which are idle, which need attention, and which have errored out. Switching between them is a context-destroying exercise in remembering session names.

**The Solution**: Swimmers turns your tmux sessions into a visual fish bowl. Each session is an animated ASCII fish. Active sessions swim, busy ones blow bubbles, sleeping ones sink to the bottom, errored ones show `x` eyes. Select a fish to inspect its pane output, open it in your desktop terminal, or read the thought stream from your AI coding agents.

### Why swimmers?

| Feature | What It Does |
|---------|--------------|
| **Aquarium view** | Sessions rendered as animated ASCII fish with state-driven sprites |
| **Live state detection** | Idle, busy, error, attention, drowsy, sleeping, deep sleep, exited |
| **Thought rail** | Side panel showing AI agent thought streams per session |
| **Native terminal handoff** | Open any session directly in iTerm or Ghostty from the TUI |
| **Mermaid diagrams** | Render and zoom Mermaid artifacts inline in the terminal |
| **Repo themes** | Per-repo sprite and color overrides via `.swimmers/theme.json` |
| **Remote API** | Point the TUI at a remote server over Tailscale or any network |
| **Prometheus metrics** | `GET /metrics` for monitoring session counts and API health |
| **No database, no Docker** | File-based persistence, single binary, tmux is the only dependency |

---

## Installation

### From crates.io

```bash
cargo install swimmers
```

That installs **two binaries** on your `PATH`:

- `swimmers` — the Axum HTTP/WebSocket API server that discovers and manages tmux sessions
- `swimmers-tui` — the terminal UI client that connects to the server and renders the aquarium

No repo checkout required.

### Prerequisites

| Dependency | Install |
|------------|---------|
| Rust toolchain | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh` |
| tmux | `brew install tmux` (macOS) or `apt install tmux` (Debian/Ubuntu) |
| Tailscale (optional) | Only needed for remote API access over a tailnet |

### From Source

```bash
git clone https://github.com/build000r/swimmers.git
cd swimmers
cargo build --release
```

Binaries land in `target/release/swimmers` (API server) and `target/release/swimmers-tui` (TUI client). You can also run `cargo install --path .` from inside the checkout.

---

## Quick Start

After `cargo install swimmers`, both `swimmers` and `swimmers-tui` are on your PATH. No clone required.

1. **Start the API server**

   ```bash
   swimmers
   ```

   The server binds to `127.0.0.1:3210` by default. It will keep running in the foreground; open a second terminal (or use `swimmers &` to background it).

2. **Open the TUI** (in a separate terminal or after backgrounding the server)

   ```bash
   swimmers-tui
   ```

   The TUI connects to `http://127.0.0.1:3210` automatically.

3. **Create some tmux sessions** if you don't have any yet

   ```bash
   tmux new-session -d -s dev
   tmux new-session -d -s logs
   tmux new-session -d -s deploy
   ```

   They appear in the aquarium within seconds.

4. **Navigate** — arrow keys to select a fish, Enter to open the session in your terminal, `q` to quit the TUI.

5. **Stop the server**`Ctrl-C` in the terminal running `swimmers`, or `kill $(lsof -ti:3210)` if you backgrounded it.

---

## Bind Address and Network Access

By default the server binds to **`127.0.0.1:3210`** (loopback only). The TUI also defaults to `http://127.0.0.1:3210`.

### Loopback (default, no auth required)

```bash
swimmers          # binds 127.0.0.1:3210
swimmers-tui      # connects to http://127.0.0.1:3210
```

### External / Tailscale access

Set `SWIMMERS_BIND` to expose the server on a non-loopback interface. The server emits a warning to stderr when binding to a non-loopback address because `LocalTrust` auth (the v0.1.0 default) grants full access to any client that can reach the port.

```bash
# Bind to all interfaces (e.g., for Tailscale access from another machine)
SWIMMERS_BIND=0.0.0.0 swimmers

# Bind to a specific Tailscale IP
SWIMMERS_BIND=100.101.123.63 swimmers

# Point the TUI at the remote server
SWIMMERS_TUI_URL=http://100.101.123.63:3210 swimmers-tui
```

When you bind externally and want real auth, set `AUTH_MODE=token` and a shared `AUTH_TOKEN`.

---

## Environment Variables

| Variable | Default | Purpose |
|----------|---------|---------|
| `SWIMMERS_BIND` | `127.0.0.1` | Server bind address (interface only, not `host:port`) |
| `PORT` | `3210` | Server listen port |
| `SWIMMERS_TUI_URL` | `http://127.0.0.1:3210` | API URL the TUI connects to |
| `AUTH_MODE` | `local_trust` | Auth mode: `local_trust` or `token` |
| `AUTH_TOKEN` | (none) | Bearer token when `AUTH_MODE=token` |
| `SWIMMERS_NATIVE_APP` | `iterm` | Native desktop target: `iterm` or `ghostty` |
| `SWIMMERS_THOUGHT_BACKEND` | `openrouter` | Thought subsystem backend: `openrouter`, `codex`, or `inproc` |
| `SWIMMERS_REPLAY_BUFFER_SIZE` | `524288` | Replay ring size in bytes (default 512 KB) |
| `SWIMMERS_FRANKENTUI_PKG_DIR` | auto-detect | Path to `frankentui/pkg` for live browser terminal rendering |

When `SWIMMERS_NATIVE_APP=ghostty`, the API uses Ghostty's AppleScript support to create or replace a left-side preview split for the selected tmux session. This path requires Ghostty 1.3.0+ on macOS with automation access enabled.

While the TUI is running, press `n` or click the top-right native-open label to switch between `iTerm` and `Ghostty` without restarting the API.

---

## Make Targets

If you are working from a source checkout, the Makefile has convenience targets:

```bash
make tui                # Start local API + TUI (one command)
make web                # Start the server for browser/tailnet access
make server             # Run only the API server
make tui-check          # Wait for an existing API, then exit
make tui-smoke          # Run shell-level bootstrap tests
make cargo-cov-lcov     # Generate lcov coverage report
```

---

## Configuration

Swimmers reads all configuration from environment variables. There is no config file. Defaults are sane for local use:

```bash
# Minimal local usage (everything defaults)
swimmers

# External access with token auth
SWIMMERS_BIND=0.0.0.0 \
AUTH_MODE=token \
AUTH_TOKEN=your-secret-token \
swimmers
```

### Repo Themes

Drop a `.swimmers/theme.json` in any repo directory to override sprite colors for sessions whose `cwd` matches that repo. The TUI discovers themes automatically.

---

## Architecture

```
┌──────────────────────────────────────────────────────────────┐
│                     swimmers-tui (client)                     │
│  Aquarium view  |  Thought rail  |  Mermaid viewer           │
│  Keyboard/mouse navigation  |  Native terminal handoff       │
└───────────────────────────┬──────────────────────────────────┘
                            │ HTTP (REST JSON)
┌──────────────────────────────────────────────────────────────┐
│                     swimmers (API server)                     │
│  Axum router  |  Auth middleware  |  Prometheus /metrics      │
├──────────────────────────────────────────────────────────────┤
│  SessionSupervisor                                           │
│    ├─ tmux discovery loop                                    │
│    ├─ lifecycle broadcasts                                   │
│    └─ persistence checkpoints                                │
│  SessionActor (per session)                                  │
│    ├─ PTY I/O via portable-pty                               │
│    ├─ replay ring buffer                                     │
│    ├─ state detection (idle/busy/error/attention)             │
│    └─ ScrollGuard (redraw burst coalescing)                  │
│  Thought subsystem                                           │
│    ├─ bridge runner (daemon mode)                             │
│    └─ loop runner (in-process mode)                           │
├──────────────────────────────────────────────────────────────┤
│  FileStore (data/swimmers/)  — flat-file persistence         │
└───────────────────────────┬──────────────────────────────────┘
                            │ PTY / shell exec
┌──────────────────────────────────────────────────────────────┐
│                         tmux server                          │
│  Sessions  |  Windows  |  Panes                              │
└──────────────────────────────────────────────────────────────┘
```

### API Endpoints

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/v1/sessions` | List tmux sessions with state |
| `POST` | `/v1/sessions` | Create a new tmux session |
| `DELETE` | `/v1/sessions/{id}` | Remove a session |
| `GET` | `/v1/sessions/{id}/snapshot` | Capture visible screen text |
| `GET` | `/v1/sessions/{id}/pane-tail` | Recent pane output |
| `POST` | `/v1/sessions/{id}/attention/dismiss` | Clear attention state |
| `POST` | `/v1/sessions/{id}/input` | Send text input to a session |
| `GET` | `/v1/selection` | Read the published selection |
| `POST` | `/v1/selection` | Publish the selected session |
| `GET` | `/v1/native/status` | Native terminal support check |
| `POST` | `/v1/native/open` | Open session in desktop terminal |
| `GET` | `/v1/dirs` | Repo/service directory browser |
| `POST` | `/v1/dirs/restart` | Restart a mapped service |
| `GET` | `/v1/skills/{tool}` | List available skills for a tool |
| `GET` | `/v1/thought-config` | Read thought runtime config |
| `PUT` | `/v1/thought-config` | Update thought runtime config |
| `GET` | `/metrics` | Prometheus metrics |

---

## Running in Background

### nohup

```bash
nohup swimmers > swimmers.log 2>&1 &
```

### systemd (Linux)

```bash
sudo tee /etc/systemd/system/swimmers.service << 'EOF'
[Unit]
Description=Swimmers Terminal Manager
After=network.target

[Service]
Type=simple
User=your-username
Environment=SWIMMERS_BIND=127.0.0.1
Environment=PORT=3210
ExecStart=/home/your-username/.cargo/bin/swimmers
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now swimmers
```

### macOS LaunchAgent

```bash
mkdir -p ~/Library/LaunchAgents

cat > ~/Library/LaunchAgents/com.swimmers.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.swimmers</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/your-username/.cargo/bin/swimmers</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/swimmers.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/swimmers.err</string>
</dict>
</plist>
EOF

launchctl load ~/Library/LaunchAgents/com.swimmers.plist
```

---

## How swimmers Compares

| Feature | swimmers | tmux ls | tmuxinator | byobu |
|---------|----------|---------|------------|-------|
| Visual session overview | Session-state-driven animated sprites | Text list | Text list | Status bar |
| State detection (busy/idle/error) | Automatic | Manual | None | Partial |
| AI thought stream | Built-in side panel | None | None | None |
| Remote access | REST API, any network | SSH + tmux attach | Local only | SSH + byobu |
| Native terminal handoff | One keypress from TUI | `tmux attach -t` | Manual | Manual |
| Metrics/observability | Prometheus `/metrics` | None | None | None |
| Setup complexity | `cargo install swimmers` | Already installed | Ruby + config files | apt install |

**When to use swimmers:**
- You run many tmux sessions and want a visual overview
- You use AI coding agents and want to see their thought streams
- You want to monitor remote sessions from a local TUI

**When swimmers is not the right tool:**
- You only use one or two tmux sessions (tmux is fine on its own)
- You need a tmux session template/layout manager (use tmuxinator)

---

## Troubleshooting

### TUI cannot reach the API

```bash
# Check if the API is running
curl -s http://127.0.0.1:3210/v1/sessions

# Start it
swimmers
```

### TUI gets 401 or 403

The API is running with token auth. Set your credentials:

```bash
AUTH_MODE=token AUTH_TOKEN=your-token swimmers-tui
```

### No sessions showing in the aquarium

Create at least one tmux session:

```bash
tmux new-session -d -s dev
```

### Port already in use

```bash
lsof -ti:3210 | xargs kill
swimmers
```

### Cargo build fails

```bash
rustup update stable
cargo clean
cargo build --release
```

---

## Limitations

- **tmux only** — swimmers does not manage screen, zellij, or plain terminal sessions
- **Browser UI is terminal-first** — the web surface is for remote attach/control; the animated aquarium remains native-only
- **Single-machine sessions** — the API manages tmux sessions on the machine it runs on; it does not aggregate sessions across multiple hosts
- **No session templating** — swimmers discovers existing tmux sessions but does not define layouts or startup commands (use tmuxinator for that)
- **macOS and Linux only** — tmux does not run on Windows, so neither does swimmers

---

## FAQ

### Why "swimmers"?

Sessions are fish. The TUI is an aquarium. Fish swim. Sessions swim between states.

### Does it need Docker?

No. Single binary, flat-file persistence, talks to tmux directly.

### Can I run the API without the TUI?

Yes. Run `swimmers` on its own and use the REST endpoints directly, open the browser UI, or point a TUI at it later.

### What happens when I close the TUI?

Your tmux sessions keep running. The API keeps running if started separately. Reopen the TUI to reconnect.

### Can multiple TUIs connect to the same API?

Yes. The API is a standard HTTP server. Point multiple TUI instances at the same URL.

### How does state detection work?

The `SessionActor` monitors each session's PTY output and classifies it into states (idle, busy, error, attention) based on shell activity patterns. Rest states (drowsy, sleeping, deep sleep) layer on top based on inactivity duration.

### What is the thought rail?

A side panel in the TUI that displays AI agent thought streams. When a session runs Claude Code, Codex, or similar tools, their internal reasoning appears in the thought rail next to the aquarium view.

### Is `LocalTrust` auth safe?

On loopback (`127.0.0.1`), yes — only processes on the same machine can reach the port. When you set `SWIMMERS_BIND` to a non-loopback address, the server warns you on startup. Use `AUTH_MODE=token` with a strong `AUTH_TOKEN` for any external exposure.

---

## Design Philosophy

**Sessions are living things.** The aquarium metaphor is not decoration. It encodes session state into spatial position, animation speed, and sprite shape so you can assess a fleet of sessions with a glance instead of reading text.

**The API is the truth.** The TUI is a client. The API discovers tmux sessions, tracks their state, and serves snapshots. You can point multiple TUIs at the same API, run the API headless, or build your own client against the REST endpoints.

**No infrastructure required.** No database, no Docker, no message broker. The server binary talks to tmux directly via `portable-pty`, persists state to flat files under `data/swimmers/`, and serves HTTP on a single port.

**Thoughts are first-class.** The thought subsystem streams AI agent context (from Claude Code, Codex, etc.) into a side panel. Sessions that run AI coding agents surface their internal monologue alongside the terminal output.

---

## About Contributions

Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. Feel free to open issues — bug reports in particular are welcome. PRs are fine as a way to illustrate a proposed fix, but I won't merge them directly; I'll have Claude or Codex review and independently decide whether and how to address them.

---

## License

MIT. See [LICENSE](https://github.com/build000r/swimmers/blob/main/LICENSE).