Daeseon Yoo
Back to project
·Tech retro·3 min

Putting tmux behind a SessionBackend trait (scaffolding, not a Windows port)

Three small parallel packets (sb01–sb03) introduced a Rust SessionBackend trait and migrated tmux helper callsites behind it. It is honest scaffolding: the renderer never changed, the PTY spawn hot-path is unchanged, and the only implemented backend is still tmux-local.

For a year the desktop app has had tmux wired straight into the Rust backend: pane spawning, scroll mode, kill, PID lookup, capture, System Pulse pane listing — all assembling tmux command lines inline in pty.rs, lib.rs, and syspulse.rs. That is fine on macOS/Linux and a wall on Windows, where there is no tmux. On 2026-06-17 three parallel packets (sb01, sb02, sb03) introduced a boundary so future non-tmux backends have somewhere to live. The honest framing first: this is not a Windows port. No ConPTY, no WSL-tmux, no native multiplexer was added. The only implemented backend is still tmux-local, and the renderer IPC contracts did not change.

sb01 — the boundary (commit f6da0ea, "add session backend boundary"). This created apps/desktop/src-tauri/src/session_backend.rs with the core model: a Platform enum (current() keyed on cfg(target_os)), a SessionBackendName (only TmuxLocal outside tests), a SessionBackendSelection, a SessionBackendError, the SessionBackend trait, and the concrete TmuxSessionBackend. Selection is deterministic and explicit: select_session_backend_for_platform resolves None | "auto" | "local" | "tmux" | "tmux-local" all to TmuxLocal, returns UnsupportedBackend for anything else, and — critically — returns UnsupportedPlatform on Windows because TmuxLocal.supports_platform is !matches!(platform, Platform::Windows). So Windows now fails loudly with a named error instead of silently spawning a dead pane. Runtime wiring stayed minimal: pty.rs pulls tmux_path() and session_name(&id) from current_tmux_backend() (session_backend.rs:418), and scroll_mode / pty_kill route through toggle_scroll_mode / kill_session.

sb02 — read callsites (commit 0ee7065, "route pane helpers through session backend"). Added pane_pid() and pane_current_command() to the trait and pointed pane_agent_provider and pane_codex_rollout_path at them, so agent-provider detection and Codex rollout recovery no longer assemble tmux queries in lib.rs. Test count moved 99 → 100.

sb03 — capture + pulse (commit 852bc47, "move pane reads behind session backend"). Added capture_text(pane_id, lines) and list_panes() (returning a backend-neutral SessionBackendPane), moving pane_capture_text off direct tmux capture-pane and System Pulse pane discovery off tmux list-panes in syspulse.rs. System Pulse stays fail-soft: an unavailable or unsupported backend yields an empty pane list, never invented metrics.

What was deliberately left alone, and why it's the honest part. The PTY spawn hot-path is unchanged. pty.rs still owns the full /bin/bash -c "..." wrapper string — the -L dalkkak server, the npm_config_* env-scrub from RULE #5b, set -g mouse/status off, new-session -A -D, and the fallback exec ${SHELL}. The backend only supplies two strings into it. The sb03 audit table marks spawn and orphan GC as intentionally deferred: spawn carries user-visible fallback-shell and env-repair behavior, and orphan GC encodes tmux session liveness semantics — both need their own contract and regression tests before a non-tmux backend can honor them, rather than being forced into tmux concepts.

So the net effect is a trait, a deterministic selector, an explicit Windows error, and ~7 read-side callsites migrated — verified by cargo test (103 passed, 4 ignored) and cargo fmt --check, with no TypeScript touched. It is scaffolding done in the cheap direction: shape the seams now, keep tmux behavior bit-identical, and resist claiming Windows support until a real backend exists and is tested. The callsite-coverage table in docs/architecture/session-backend-boundary.md is the running ledger of what is and isn't behind the seam yet.