Skip to content

Decide: should tmux return as a selectable Coder engine backend? (real PTY + attach) #249

Description

@serge-ivo

Rescoped 2026-08-06. #247 has since shipped — the vestigial tmux naming is gone (engineLabel replaces tmuxSession across the API surface). So the "or finish removing the naming" half of this is done, and what remains is the single question: should tmux come back as a selectable engine backend? Everything below still applies to that question.

Direction check: the proposal below adds backend: "stream-json" | "tmux" — a new closed enum on engine presets — while #160 is explicitly about retiring closed enums (capabilities.workflow) in favour of declarative composition. If tmux does come back, it should probably arrive the declarative way rather than as one more hardcoded alternative.


First, the thing that is easy to conflate

tmux is already a platform connector and always has been. lib/connectors/registry.ts:82
declares tmux (tmux_list_sessions, tmux_capture_pane, tmux_run_command), and :72 declares
the generic terminal connector, whose local adapters are tmux, kitty remote control and iTerm2
AppleScript. Both are live, both reach the machine over the runner relay, and the tmux / Terminal /
kitty / iTerm2 Operator agents use them today.

So this ticket is not "add tmux to the platform". It is: restore a tmux-backed engine for
Coder sessions
, as a selectable alternative to the stream-json child process — the thing that was
deleted.

What was removed, and when

Commit c94642f, 2026-06-26 — "feat(coding): drive Claude Code via stream-json — one structured
engine, no tmux"
. It deleted packages/browser-runner/src/coding/session.ts (180 lines) and
session.test.ts (110 lines), and pointed CodingRuntime at the new HeadlessSession.

The deleted CodingSession implemented the same surface CodingRuntime still expects:

method tmux implementation
start() createSession(name, workDir), export env vars via sendText, then sendText(cliCommand)
snapshot(lines) capturePane(target, lines)
input(text) sendText
interrupt() / stop() send keys / killSession
runState() inferred by regex over the captured pane

Every tmux primitive it relied on still exists and is still maintained
coding/tmux.ts was kept for the terminal connector (createSession, capturePane, sendText,
sendKey, killSession, sessionExists, sanitizeSessionName). So the restore is mostly
re-adding one class, not rebuilding a subsystem.

CodingRuntime.start() (runtime.ts:126) currently constructs new HeadlessSession({…})
unconditionally — no flag, no branch. That single call site is where a backend choice would go.

What the removal traded away, and what has since changed

That commit stated its trade-offs explicitly:

Trade-offs (deliberate): drops the multi-CLI tmux path (gemini/codex/grok) and live-TTY
takeover — both replaced by structured messaging, which fits headless.

One of those has already been reversed by other means: multi-CLI came back. Codex, Grok and
Gemini now run through HeadlessSession as raw spawns with stdout capture (headless.ts, mode: "raw"), so "tmux is needed for non-Claude engines" is no longer true.

What is still genuinely missing without tmux:

  • A real PTY. Raw-spawn engines get a pipe, not a TTY. A CLI that requires a terminal (full-screen
    TUI, prompts that need isatty, colour/width detection) cannot work on the current path.
  • Live human takeover of the actual terminal — attaching to the same session the agent is driving.
  • tmux attach from the user's own machine to watch or intervene directly. Users currently
    expect this, because every session still shows a pags-<engine>-<id> name (Coding sessions no longer use tmux, but the names, diagnostics, kill-tmux control and docs all still say they do #247).
  • Survival of a runner crash for non-Claude engines. Claude survives via --resume from
    ~/.claude; a raw spawn dies with the runner, where a tmux session would outlive it.

What it would cost

Honest accounting, since this is re-adding something that was deliberately deleted:

  • A second engine implementation to maintain behind one interface, and every coding feature must
    then work on both.
  • Run-state goes back to regex over a scraped pane for that backend — the guessing the commit
    message called out, and the source of the premature-finish bugs it fixed.
  • The console's terminal view, /capture, takeover, the Pilot loop and the orphan reconciler all
    need to be correct for both backends.

Proposal

Decide before building. The value case is a real PTY, real attach, and surviving a runner crash
for non-Claude engines. If those are not wanted, the better outcome is to finish #247 — drop the
tmux naming entirely — rather than carry two engines. This ticket exists to make that a decision
rather than an accident of history.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3: laterDesign, strategy, or deferred pending a decision or demandcoderThe Coder wedge agent (#68) — Engine, Pilot, Co-pilot, Loop, OverseerenhancementNew feature or requestrunnerpackages/browser-runner + packages/cli — ships only via an npm publish

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions