fux 0.2.0

Agent-native terminal workspace
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
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# The Shape of fux

An agent workspace: a small native terminal multiplexer built directly on koh's per-pty emulator and
mosh-grade peer-to-peer attach path, with agent-state detection delegated to zor, a separate wrapper
program around each pane (see `wrapper-design.md`). One `cargo install`-able binary, one process, no sandbox.

- **Status:** proposal, audited against source. Supersedes the zellij-based draft of 2 Sep 2026.
- **Date:** 3 Sep 2026
- **Reference trees:** `references/` — koh 0.12.0 (5e8620b) · herdr 0.8.2 (94f6d9c,
  github.com/herdrdev/herdr) · zellij `main` at 0.46.0 (af38660), kept for grid edge cases only
- **Dependency:** published koh **0.12.0** (fux pins
  `=0.12.0`, `default-features = false`, `backend-termina`) · fux 0.1.0 placeholder. herdr proper is not published; the `herdr`
  crate on crates.io is unrelated.

---

## Why not zellij

The previous draft embedded fux in zellij: fux as the binary, zellij's client and server as library
crates, detection as a wasm plugin, koh hosting zellij's client in a pty. It was buildable, and every
one of its risks came from the seam:

| Cost of the zellij route | Native route |
|---|---|
| Detection interpreted on wasmi, on a copy of pane text, behind a permission cache hack | Detection is a separate wrapper program; fux reads one OSC |
| Two vt100 passes per frame: zellij emulates each pane, koh re-emulates zellij's output | koh's emulators *are* the panes; one compositing pass |
| Exact pins on crates that exist to serve one binary; 1.95 toolchain floor; committed wasm artifact; CI wasm target; a pipe protocol | None of it |
| The one risk that could sink the design: zellij's client rendering cleanly under koh's grid | Nothing renders under anything |
| Phone alerts via bell or title tricks through the plugin pipe | State is native; the phone gets a field on the existing diff |

What zellij offered that fux does not need: floating panes, KDL layouts, a plugin system, mouse
resize, session manager UI, years of grid polish. What fux gives up: that polish, and users' zellij
muscle memory. An agent workspace is a fixed set of long-running CLIs in splits with a status line.
That is a small multiplexer, and koh already contains most of one.

**What koh already is.** A pty spawner, a server-side `vt100::Parser` per pty with title, bell,
clipboard and exit-code callbacks, a diff engine that ships screen deltas, a state-sync protocol that
survives disconnects, session retention, identity and allow-lists, a phone client, and predictive
echo. That is one pane of a multiplexer plus the entire transport. koh's terminal, server, client and
pty layers total about 5.6k lines; zellij's pane and tab code alone is over 32k.

---

## Architecture: one binary at both ends, the workspace is the synced state

```
  ANY TERMINAL (fux client)                          HOST (fux server)
  ┌──────────────────────────────┐   iroh QUIC     ┌───────────────────────────────────────┐
  │ WorkspaceState (replica)     │ <─datagrams──── │ WorkspaceState (authoritative)         │
  │  layout · tabs · focus ·     │  WorkspaceDiff: │  layout · tabs · focus · status ·       │
  │  status · per-pane grids     │  changed pane   │  per-pane grids ← Pty + ServerTerminal │
  │            │                 │  cells, layout  │            ▲              ×N            │
  │  compositor (ratatui)        │  edits          │  input router · state reader · notifier │
  │  + prediction on focused pane│ ─keys, resize─> │  control socket · workspace manager     │
  │            │ Buffer diff     │                 └───────────────────────────────────────┘
  │  real terminal (KohBackend)  │                    koh: ssp · transport_iroh · pty ·
  └──────────────────────────────┘                    terminal · predict — generic over state
```

The same binary runs on the phone, the laptop and the host. What crosses the network is not a
screen but the workspace: a layout tree per tab, the focused pane, status segments, and for each
pane the cells of its visible viewport. koh's `Transport<Local, Remote>` is already generic over
any `SyncState`, so this is a new state type with a diff, not a new protocol.

| Layer | Runs on | Built from | New code |
|---|---|---|---|
| **Panes** | host | koh `pty::Pty` + `terminal::ServerTerminal`, one pair per pane. Bytes in, `vt100::Screen` out, with OSC 0/1/2/52/9;4 and BEL captured by callbacks. | none |
| **Workspace** | host | `WorkspaceState` and its diff; BSP layout designed fresh (herdr's as the reference); tabs; input router decoding keys and SGR mouse; scroll and copy mode viewports; agent-state OSC reader; notifier; control socket; workspace manager. | ~5k lines |
| **Transport** | both | koh `ssp::Transport`, `transport_iroh`, session retention, allow-lists, keys, made generic over the synced state and the host on the server side and the renderer on the client side. | koh 0.11.0 (shipped) |
| **Client** | any terminal | ratatui compositor over the replica: panes as widgets, borders, tab bar, status line, popups; ratatui's `Buffer` diff drives the real terminal through a `Backend` over koh's `KohBackend`; koh's predictor on the focused pane's grid. | ~1.5k lines |

### Why the workspace, not the screen, is what syncs

Three tiers were on the table. Escape bytes into a `vt100::Parser` (what a pty does) throws
ratatui's diff away and rebuilds it. A composited cell grid as the state removes the parse round
trip but still resends every cell that moves when a split closes or a tab switches. Syncing the
workspace itself sends only the cells of the panes that changed; layout edits are a few bytes; a
tab switch to already-seen panes sends nothing. It also lets each client fit the layout to its own
terminal, which is the only way a phone shows a laptop-sized workspace sensibly. ratatui's buffer
diff then does exactly what it exists for: minimal repaint of a real terminal.

`WorkspaceState`: tabs (each a layout tree of pane ids, with zoom), focus, status segments, title,
bell count, popups as floating panes, and `panes: map<PaneId, PaneView>` where `PaneView` is the
visible rows of the pane at its current viewport (scrolled or live), cursor, modes and agent state.
`WorkspaceDiff` is layout edits plus per-pane changed-cell runs against the base, with a full pane
grid when the base does not have that pane at that size. `SyncState` needs `diff_from`, `apply`,
`PartialEq`, a decode limit and a resource budget; the budget is cells summed over panes, as
`TerminalScreen`'s is today.

### What koh has to become generic over

- **Server:** `serve` takes a host that produces the state. Today's host is a pty whose
  `ServerTerminal` snapshot is a `TerminalScreen`; fux's host is the workspace manager whose
  snapshot is a `WorkspaceState`. The host trait is `snapshot() -> S`, `input(&[u8])`,
  `resize(rows, cols)`, `changed: Notify`, `exited() -> Option<u32>`. `run_attached`'s snapshot
  gating, input coalescing and reaping apply unchanged.
- **Client:** `connect` takes a renderer for the remote state. Today's renderer is `render.rs`
  painting a `TerminalScreen` cell by cell; fux's is the ratatui compositor. `KohBackend` stays the
  terminal seam.
- **Prediction:** `predict.rs` reads cells from a `vt100::Screen`; it moves to a cell-reader trait
  so it can run over the focused `PaneView`.
- **Shared sessions:** all authorized peers attach to the same host instead of one session per
  endpoint id, with a per-client viewport size reported back so the host can size ptys. Pane sizes
  follow the workspace's own geometry, chosen by the host from the attached clients (last resize
  wins for v1); a smaller client clips or zooms.

The pty path keeps `TerminalScreen` and today's `koh` binary unchanged. Everything above is
additive and shipped as koh 0.11.0 (3 Sep 2026): `serve_with`/`connect_with` with the state type
selected by ALPN, `SessionHost`/`HostProvider`/`SharedHost`, `ClientState`, `predict::ScreenView`,
`ServerTerminal::progress()`/`take_unhandled_oscs()`, and `ConnectConfig::bell_command`.

### Local attach is remote attach over loopback

`fux` finds or starts the server, then attaches over iroh on the local machine. One code path for
local and remote; detach, reconnect and prediction come free. A unix-socket channel behind the
`IrohChannel` seam is a later optimisation. The server daemonises on first `fux`; `fux serve` in the
foreground is the same server under a supervisor.

---

## Surface: what the binary does

```sh
fux [name]                   # attach to a workspace, creating it and the server if needed
fux serve [--allow <id>]     # run the server in the foreground; --allow exposes it to a peer
fux connect <id>             # attach to a remote workspace, mosh-style (koh connect)
fux id                       # print this machine's endpoint id (koh id)
fux key passwd | info        # identity key management (koh key)
fux ctl <command> [args…]    # drive the running workspace over the control socket (see Control)
fux new | split | focus | …  # the common control commands, also exposed as top-level verbs
```

`id`, `key` and `connect` are koh's `run_id(IdConfig)`, `keycmd::run(KeyConfig)` and
`connect(ConnectConfig)` behind fux's own clap layer. `serve` is koh's `serve(ServeConfig)` with the
host set to the in-process workspace. Everything else is a thin client for the control socket.

Inside a workspace, a tmux-style prefix (default `Ctrl-a`) then: `|` `-` split, `hjkl` focus,
`x` close, `c` new pane, `t` new tab, `n` `p` next and previous tab, `z` zoom, `[` scroll and copy
mode, `d` detach, `s` workspace picker, `?` help. Mouse click focuses a pane; wheel scrolls the
pane under the cursor; Shift-drag selects text. Everything else goes to the focused pty verbatim. Configuration
is a TOML file with the bindings and the default command; there is no layout language.

---

## Reference code is studied, not copied

herdr and zellij are reference material, the way vt100's source is. Nothing from either tree is
ported verbatim, `layout.rs` included. For each subsystem the method is the same: read the
reference implementation for what it gets right (the invariants, the edge cases it learned the hard
way, the constants it tuned), write those down, and then design the fux version from scratch as
the most idiomatic Rust that satisfies them. The reference is a test oracle and a checklist of
behaviour, not a starting point for the code.

What that means concretely:

- **Layout tree.** herdr's `TileLayout` is a `Box<Node>` binary tree with `f32` ratios, a `Vec<bool>`
  path to address a split, global `PaneId` allocation, and a `prev_focus` field guarded by comments
  about which methods may touch it. fux keeps the invariants (BSP, ratio per split, focus survives
  close, directional navigation by geometry) and designs its own representation: an arena of nodes
  with typed indices so the tree is `Clone`, serialisable into the synced state, and free of
  recursive `Box` walks; a `NonZeroU16` ratio in fixed point so the diff is exact; a focus history
  that the type system keeps consistent instead of a comment. herdr's `find_in_direction` geometry
  cases and its tests are the acceptance suite.
- **Detection.** Lives in the wrapper, not in fux; `wrapper-design.md` applies the same rule
  there. herdr's manifests are read to learn which signals each agent emits, and the wrapper
  writes its own rules in its own schema from captured panes. Nothing from herdr enters either
  tree.
- **Grid and copy mode.** zellij's grid edge cases (wide glyphs at the right margin, wrapped-line
  selection, scrollback with resize) are read for the cases, and fux's `PaneView` and copy mode are
  written to handle them, with a test per case.
- **Anything else.** The same rule: extract the behaviour, discard the code.

The cost is that nothing arrives for free; the layout tree in particular is a week that a copy
would have saved. The gains are one consistent style across the crate, nothing Apache-2.0 to
attribute, no carried-over design debt, and a codebase the
maintainer understands end to end because they wrote it.

---

## Control, not plugins

fux has no plugin system. What multiplexer plugins do splits into four capabilities: observe
(events, pane text), act (split, focus, send keys), draw (status segments, pickers, overlays), and
intercept input (modal keys, remapping). zellij's wasm sandbox exists to hand those four to untrusted
third-party code safely and portably, which is right for a project with a plugin ecosystem and wrong
for a personal tool where every author is you or an agent you are running. herdr's answer, a socket
API driven from its CLI, is the one fux adopts.

The line is drawn by latency: **whatever must be synchronous with input or rendering lives in
core; everything else is a process.** In core: the prefix key table, scroll and zoom modes,
the OSC 7877 reader (it runs on every pane drain), the compositor, the status line renderer. Outside: anything that can be a command plus an event subscription. A program
that wants a UI runs in a pane or a popup and gets a real terminal, which is more than any plugin
drawing API offers.

### The socket

A unix socket, mode 0600, at `$XDG_RUNTIME_DIR/fux/<workspace>.sock` (or the private macOS
`$HOME/Library/Caches/fux-runtime/fux` fallback), speaking newline-delimited
JSON. Every message carries an `id`; the server answers with the same `id`. Two message kinds:

**Commands** (request, one reply):

| Command | Effect |
|---|---|
| `new [--cwd DIR] [-- argv…]` | open a pane; returns its id |
| `split <h\|v> [--target ID] [-- argv…]` | split a pane and run a command in the new half |
| `focus <ID\|left\|right\|up\|down>` | move focus |
| `zoom [ID]` | toggle the focused (or given) pane filling the screen |
| `kill <ID>` | close a pane and its process |
| `resize <ID> <+n\|-n>` | grow or shrink along the split axis |
| `send-keys <ID> <bytes>` | write to a pane's pty; keys as text or as `\x1b` escapes |
| `capture <ID> [--attrs] [--scrollback N]` | pane text, plain or with cell attributes, optionally with history; captured text is capped at 128 KiB so the encoded reply fits the 1 MiB frame |
| `tab <new\|next\|prev\|N>`, `workspace <list\|new\|kill>` | tab and workspace management |
| `list` | workspaces, tabs, and panes with id, command, pid, cwd, title, agent, state, geometry, focus |
| `set-status <segment> <text>` | write a named status-line segment; empty text removes it |
| `popup [--size WxH] -- argv…` | run a program in a centred overlay pane until it exits |
| `subscribe [events…]` | turn this connection into an event stream (below) |

**Events** (stream on a subscribed connection, one JSON object per line):

| Event | Payload |
|---|---|
| `pane.opened`, `pane.closed` | id, command, exit status on close |
| `pane.focused` | id |
| `pane.title` | id, title |
| `agent.state` | id, agent, old state, new state, timestamp |
| `pane.output` | id; rate-capped to one per pane per 250 ms; a nudge to `capture`, not the data |
| `workspace.resized` | rows, cols |
| `client.attached`, `client.detached` | endpoint id or `local` |

An agent that must wait until another agent is genuinely blocked is one `subscribe agent.state`
and a filter. A session picker is `popup -- fzf` fed by `list`. A "notify me on Slack" companion is
a twenty-line script.

### Bindings and hooks

The TOML config binds prefix keys to either built-ins or external commands. An external command
runs with `FUX_PANE`, `FUX_SOCKET` and `FUX_CWD` in its environment, so a binding is the same
program a user would run from a shell. `hooks` is a list of commands the server starts on boot and
restarts on exit; that is herdr's daemon shape without a separate concept, for people who want a
long-lived companion.

### What this costs

A companion process can be slow or dead, so the core must be complete without it. fux with nothing
attached to the socket is fully usable, which is not true of a zellij layout that references a
plugin that failed to load. Remote viewers over koh receive the screen and send keys; they never see
the socket. If the phone ever needs control it goes through a binding, not a network API.

---

## Detection: a separate program, one OSC to read

Agent-state detection is not fux code. It is a small dedicated wrapper, designed in
`wrapper-design.md`, that runs each pane's shell in a pty, keeps its own `vt100::Screen`,
identifies the agent from the pty's foreground process group, evaluates its rules with herdr-grade
hysteresis, and announces the result in-band as

```
ESC ] 7877 ; state=<working|blocked|idle|none> ; agent=<id> ; seq=<n> ST
```

fux's whole involvement:

- **Spawn.** Every pane is `zor --title never -- <command>`; the zor path and the
  default command are config. fux draws its own status, so titles are left alone.
- **Read.** koh's `ServerTerminal` keeps unhandled OSCs in a ring (`take_unhandled_oscs()`, 16 ×
  256 bytes, koh 0.11). On each pane drain fux drains that ring, parses OSC 7877 payloads in
  `seq` order, and sets the pane's `agent` and `state` in `WorkspaceState`. Any other payload is
  discarded.
- **Use.** The status line and tab bar show agent and state; the notifier fires on transitions
  into blocked and idle. The control socket emits the same transitions as events.

fux carries no rules, no regex, no hysteresis and no process-tree code, and needs no change when
an agent's UI changes. A pane whose program emits OSC 7877 itself, with no wrapper, works the
same way. If the wrapper is not installed, panes run bare and show no agent state; nothing else
degrades.

---

## Notifications

State transitions are native events on the server, so:

- **Desktop:** the notifier calls `notify-send` on Linux and `terminal-notifier`, falling back to
  `osascript`, on macOS, the two paths herdr uses. Runs in the server process, which is where the
  panes are, regardless of whether anyone is attached.
- **Phone:** the fux client holds the workspace replica, agent state included, so it notifies
  directly: `termux-notification` on Termux, the same notifier module as the host elsewhere. No
  bell or title tricks. Plain `koh connect` users still get the bell hook.
- **Status line:** every pane shows agent and state; blocked panes are highlighted; the tab-less
  v1 status line is one row.

---

## Distribution: one crate, one build, nothing to embed

- `cargo install fux` builds the whole thing everywhere: koh's library tree, the tiler, the OSC reader
  and the control socket. No features to choose, no wasm, no committed artifacts, no
  `include_bytes!`. A phone and a desktop run the same binary and can each host a workspace or
  attach to the other's.
- On Termux the build is koh's plus ratatui-core and the tiler, all pure Rust. koh's install notes
  (`pkg install rust clang pkg-config`) and its Android test suites apply directly. Termux ships
  rust 1.98, above koh's 1.91 floor.
- Prebuilt `aarch64-linux-android` binaries are a convenience once fux is public; `cargo install`
  is the same build.

The toolchain floor is koh's 1.91 (iroh 1.0). fux declares the same `rust-version`.

---

## Licensing

| Component | License | Use in fux |
|---|---|---|
| koh | MIT (0.11.0) | library dependency |
| herdr | Apache-2.0 | reference only |
| zellij | MIT | reference only |
| fux | MIT | |

Nothing from herdr or zellij enters the tree, so no attribution or NOTICE entry is needed; fux is
plain MIT with koh as its only third-party dependency of note.

---

## Risks

### vt100 fidelity is load-bearing for every pane — *tested, retired*

Every agent CLI renders through koh's `vt100`. Tested 3 Sep 2026 with plain koh from a phone:
Claude Code renders correctly. Where a later CLI falls short, koh owns the pin and can patch or
fork vt100; it is a 5k-line crate.

### Input decoding does not exist in koh — *build it in the router, host-side*

koh's client is a raw byte pass-through: no key decoding, no mouse parsing, no kitty keyboard
protocol; its one interception is the `Ctrl-^` escape. That is the right shape for the phone, so the
router decodes on the server: prefix sequences, SGR 1006 mouse reports, bracketed paste. The router
sees the same bytes whether they came from the local terminal or the phone.
**Mitigation:** decode conservatively and pass anything unrecognised through to the focused pane
unchanged, so an unknown sequence degrades to tmux-like behaviour rather than being eaten.

### Two viewers, one grid — *last resize wins, for now*

A phone at 40×90 and a desktop at 200×60 cannot both see the workspace at native size.
**Mitigation:** follow the most recent resize, which koh's coalescing already implements; each pty is
resized to its pane. Add tmux's smallest-client rule when it hurts.

### Diff cost — *bounded and measurable*

`diff_from` compares each pane's grid against the base, linear in visible cells; a 200×60
workspace is 12k cells. The client's ratatui diff is the same size. koh's snapshot gating already
rate-limits how often the host is asked for a state.
**Mitigation:** keep a per-pane dirty flag from the drain so unchanged panes are skipped in both
diff and snapshot; measure with the chaos harness before optimising further.

### koh 0.11 is a real release, not a patch — *shipped 3 Sep 2026*

Generic server and client, the host trait, shared sessions, the predictor trait, OSC 9;4 capture,
the bell hook. All additive, the pty path and wire protocol (`PROTOCOL_VERSION` 3) unchanged, but
it was the largest koh change since the backend seam. It is on crates.io as 0.11.0; fux pins it
exactly. Retired as a risk.

---

## Read from source

All paths relative to `references/`.

- `koh/src/lib.rs:36`, `:41` — public API stability: the four config types, entry points, and `ssp::{SyncState, Transport}`; everything else unstable
- `koh/src/lib.rs:32` — layering law: `wire ← ssp ← {terminal, input}`; `server`/`client` never `use crate::wire`
- `koh/src/terminal/mod.rs:128`, `:259`, `:307`, `:327` — `TerminalScreen`; `ScreenDiff`; `diff_from` = `state_diff`; `apply` feeds bytes to a parser
- `koh/src/terminal/mod.rs:97`, `:203` — `blank_screen` and `from_bytes`: every screen comes from a `vt100::Parser`
- `koh/src/terminal/server.rs:33`, `:107`, `:150`, `:251` — `Callbacks` (OSC 0/1/2/52, BEL); `ServerTerminal`; `process`; `snapshot`
- `koh/src/ssp/mod.rs:82` — `SyncState` trait bounds; `ssp/transport.rs:52` — `Transport<Local, Remote>` is generic over any state
- `koh/src/ssp/mod.rs:43` — scheduler constants: 20–250 ms send interval, 32 sent states, 1024 received
- `koh/src/server/session.rs:31`, `:53`, `:59`, `:90`, `:141` — `Session { emu, pty, … }`; sessions keyed by peer `EndpointId`; `spawn_session`; `drain`; `attach`
- `koh/src/server/mod.rs:101`, `:120`, `:246`, `:293`, `:299` — resize coalescing keeps the last; `ServerSession`; `run_attached` loop; `pty.write_input`; pty + emu resize
- `koh/src/server/cli.rs:45`, `:54`, `:55`, `:74` — `ServeConfig`; `command` argv; `scrollback` (default 1000, max 1 000 000); `DEFAULT_SESSION_TTL_SECS = 86_400`
- `koh/src/pty.rs:43`, `:138`, `:156`, `:275`, `:287`, `:300` — `build_command`; `Pty::spawn(rows, cols, argv, term)`; `TERM`; `write_input`; `resize`; `try_wait`
- `koh/src/client/cli.rs:321`, `:337` — raw stdin passthrough thread; SIGWINCH resize
- `koh/src/client/mod.rs:40`, `:392` — the `Ctrl-^` escape machine, the client's only key interception
- `koh/src/client/render.rs:24`, `:158`, `:246`, `:250` — cell-by-cell repaint; `WindowState`; bell on count increase; input modes re-asserted locally (mouse passes through undecoded)
- `koh/src/client/backend/mod.rs:106` — `KohBackend` trait; termina, crossterm, qwertty impls
- `koh/src/predict.rs` — local-echo prediction, no `crate::` imports; reusable per pane
- `koh/src/wire.rs:42`, `:34`, `:96`; `transport_iroh/mod.rs:58`, `:620` — `PROTOCOL_VERSION = 3`; 1200-byte datagrams; 16 MiB decode cap; ALPN; `IrohChannel` over unreliable datagrams, no streams
- `koh/Cargo.toml` — `iroh = "=1.0.0"`, `vt100 = "=0.16.2"`, `portable-pty = "=0.9.0"`; `rust-version = "1.91"`; `unsafe_code = "forbid"`, `dead_code = "deny"`, clippy panic denies; `panic = "unwind"` required for vt100 containment
- `koh/README.md:73` — "As a library"; `:109` — no scrollback sync, no Windows
- `vt100-0.16.2/src/screen.rs:113`, `:148`, `:273`, `:534` — `set_scrollback` viewport; `rows`; `rows_formatted`; `cell`
- `herdr/src/layout.rs:1`, `:73`, `:84`, `:350` — BSP tree: `Node`, `TileLayout`, `find_in_direction`; depends only on ratatui `Rect`/`Direction`
- `herdr/src/detect/`, `herdr/src/pane/agent_detection.rs` — read for the wrapper; see `wrapper-design.md`
- `herdr/src/platform/linux.rs:554`, `macos.rs:547`, `:643` — `notify-send`; `terminal-notifier` first, `osascript` fallback
- `herdr/build.rs:6`, `Cargo.toml` — libghostty-vt built with zig: why herdr's emulator is not reused; ratatui 0.30, the version fux's compositor targets
- `herdr/src` — 224k lines total; `client/` 35k, `server/` 22.5k, `pane/` 10.6k, `detect/` 5.3k

---

## Open questions

- **Execution clarification (3 Sep 2026):** focus and zoom are per-tab, while pane ids remain
  workspace-global. Named workspaces use one iroh endpoint per workspace plus an atomic local
  descriptor; this preserves koh's one-host-per-ALPN connection model without modifying koh.
- **Execution clarification (3 Sep 2026):** detach is encoded client-side as koh's `0x1e '.'`
  sequence because `SessionHost::input` does not identify the sending client. Scrollback and
  capture clone `ServerTerminal::snapshot().screen()` and move only the clone's viewport.

None blocking. Everything raised in the audits of 2 and 3 Sep 2026 is settled below.

### Settled

- **koh's stable API covers everything but the generic seams.** Config types with public fields,
  `Transport` already generic over the state, clap off by default. The host trait is
  `snapshot() -> S`, `input`, `resize`, `changed`, `exited`; the pty host stays as it is. Decided
  3 Sep 2026.
- **A composited `vt100::Screen` cannot be built from cells**, only from a parser; one reason
  the synced state is the workspace, not a screen.
- **Input decoding is fux's job on the host.** koh's client stays a byte pipe for keys.
- **herdr's UI and emulator are not reusable.** libghostty-vt via zig and ratatui. Its layout
  tree and notifier paths are studied and redesigned, not ported; its detection model moves to
  the wrapper.
  Decided 3 Sep 2026.
- **Termux clears the floor.** rust 1.98 on termux-packages `master`, koh's floor is 1.91.
- **No plugin system.** A local control socket with commands and events, panes and popups as the
  UI surface, config bindings and hooks for the rest. See *Control, not plugins*.
- **ratatui paints on the client.** `ratatui-core` for `Buffer`, `Rect` and `Layout`,
  `ratatui-widgets` for borders, tab bar, status line and popups; not the umbrella crate. A fux
  `Backend` over koh's `KohBackend` turns ratatui's buffer diff into the real terminal repaint.
  Each pane is a widget over its `PaneView` grid, marking wide-glyph continuations as skip cells.
  Panes stay `vt100::Screen` on the host; ratatui is never the emulator. fux's layout tree
  targets ratatui's `Rect` directly, as herdr's does. Pure Rust; builds on Termux.
- **Sync the workspace, composite on the client** (tier A above). Decided 3 Sep 2026.
- **Prediction per pane on the client, in v1.** koh's predictor over the focused `PaneView` via a
  cell-reader trait. Decided 3 Sep 2026.
- **Windows is out of scope**, as in koh. Decided 3 Sep 2026.
- **Named workspaces, like tmux sessions.** One server per user hosts many workspaces; `fux
  [name]` attaches or creates, bare `fux` opens a picker (or the only workspace). Decided 3 Sep 2026.
- **Tabs in v1.** Each workspace holds a list of layout trees with a tab bar in the status line;
  `zoom` is a per-tab toggle and the phone's default view. Decided 3 Sep 2026.
- **OSC 9;4 progress via `ServerTerminal::progress()` in koh 0.11** (planned as an `unknown_osc`
  callback; shipped as a host-side accessor). The wrapper reads progress from its own screen;
  fux reads only OSC 7877 from the same ring. Decided 3 Sep 2026.
- **Bell hook `--on-bell` on `koh connect`** (`ConnectConfig.bell_command`) for plain koh users
  on Termux. fux's own client sees agent state in the replica and notifies directly. Decided
  3 Sep 2026.
- **Copy mode in v1 includes mouse selection.** Scroll viewport, keyboard selection, and
  drag-select with SGR mouse; copy goes out over koh's existing OSC 52 path. A pane that has
  mouse reporting on gets the mouse unless a modifier (default Shift) claims it for selection,
  the xterm convention. Decided 3 Sep 2026.
- **No control API versioning in v1.** Decided 3 Sep 2026: no version field and no handshake
  until something actually changes; scripts written against v1 are on notice.

---

## Your side: koh and everything else outside this repo

### koh (0.11) — *shipped as 0.11.0, 3 Sep 2026 (PR #14)*

- [x] **Generic server.** `serve_with` over `S: SyncState` and `SessionHost` (`snapshot`, `input`,
      `resize(client, ..)`, `stamp_echo_ack`, `alive`, `attach_notify`, `client_detached`);
      `PtyHost` yields `TerminalScreen` as today. State type selected by ALPN.
- [x] **Generic client.** `connect_with` over `ClientState`; `ClientTerminal<S>::render` and
      `client::InputModes`. `KohBackend` unchanged.
- [x] **Shared sessions.** `SharedHost` and `ClientId`; every peer attaches to one host and
      `resize` carries the client id so the host can choose pane geometry.
- [x] **Predictor over `predict::ScreenView`** instead of `vt100::Screen` directly.
- [x] **OSC 9;4 progress** via `ServerTerminal::progress()`; other unhandled OSCs via a bounded
      `take_unhandled_oscs()` ring instead of an `unknown_osc` callback.
- [x] **Bell hook on the client.** `--on-bell <cmd>` / `ConnectConfig::bell_command` /
      `client::BellHook`, rate-limited to one spawn per second.
- [x] **Agent-CLI rendering test** with plain koh from a phone: Claude Code renders correctly.
- [ ] Optional: **local channel** behind the `IrohChannel` seam for unix-socket attach (not in
      0.11; local attach goes over iroh loopback).

### herdr

- Nothing. herdr is reference material only: no code, no manifest data, no attribution.

### The wrapper

- [ ] **Build it**, per `wrapper-design.md` and `wrapper-prompt.md`, alongside fux per
      `build-prompt.md`; fux's pane spawn and OSC reader are written against its contract. Crate
      name: `zor`.

### crates.io and accounts

- [x] **`fux` is already yours** (0.1.0 placeholder, 18 Aug 2026; implementation releases from 0.2.0).
- [x] **Public remote for fux.** `references/` stays gitignored; CI uses released koh source. Nothing is
      needed from the reference trees.
- [ ] **CI:** stable Rust at 1.91 floor, macOS and Linux; `aarch64-linux-android` cross build for
      the phone binary once fux is public. No wasm target.

### Decisions already made

- fux is one crate, one binary, one build. No Cargo features; phone and desktop builds are
  identical and each can host or attach.
- fux stays MIT; koh is MIT as of 0.11.0.
- Build the multiplexer, do not embed zellij. Depend on koh 0.11.0 as a library, generic over the
  synced state with the state type selected by ALPN.
- The synced state is the workspace; the client composites with ratatui-core and ratatui-widgets,
  no ratatui backend crate.
- All workspace logic is on the host; the client renders, predicts, and selects.
- Local attach uses the same transport as remote attach.
- Reference code is studied, not copied: herdr and zellij supply invariants, edge cases, constants
  and test oracles; every fux subsystem is designed fresh as idiomatic Rust. No herdr code or
  data is vendored.
- Agent-state detection is a separate program. fux spawns panes through it and reads OSC 7877;
  the wrapper is designed in `wrapper-design.md`. Decided 3 Sep 2026.
- Programmatic control over a unix socket instead of plugins; the CLI is a thin client for it.
- Named workspaces, tabs, zoom, and mouse selection are all v1.